By | 2024년 11월 9일
Table of Contents

Core Data – NSFetchedResultsController 가이드

Core Data를 사용해 대량의 데이터를 UITableViewUICollectionView에 뿌려줘야 할 때, 효율성과 성능을 모두 잡을 수 있는 최고의 도구가 바로 NSFetchedResultsController입니다.

1. NSFetchedResultsController란?

NSFetchedResultsController(FRC)는 Core Data 검색 결과와 사용자 인터페이스(UI)를 동기화하는 ‘중개자’ 역할을 합니다. 단순히 데이터를 가져오는 것을 넘어, 데이터셋의 변화를 감지하고 이를 테이블뷰나 컬렉션뷰에 즉각적으로 반영하는 기능을 제공합니다.

2. 왜 사용하는가? (주요 장점)

단순히 fetchRequest를 수행하는 것보다 FRC를 사용하면 다음과 같은 이점이 있습니다.

  • 메모리 효율성: 모든 데이터를 한꺼번에 메모리에 올리지 않고, 필요한 부분만 가져와서 보여줍니다.
  • 섹션 관리: 데이터를 특정 기준에 따라 섹션(Section)별로 아주 쉽게 나눌 수 있습니다.
  • 자동 업데이트: 데이터베이스에 변경(저장, 삭제, 수정)이 생기면 Delegate 메서드를 통해 UI를 자동으로 갱신합니다.
  • 캐싱: 복잡한 정렬이나 섹션 계산 결과를 캐싱하여 성능을 최적화합니다.

3. 핵심 구성 요소

FRC를 구현하기 위해 필요한 4가지 핵심 요소입니다.

요소 설명
Fetch Request 어떤 데이터를 가져올지 정의 (Entity, Sort Descriptors 필수)
Managed Object Context 데이터를 가져올 통로 (MOC)
Section Name Key Path 데이터를 섹션별로 나눌 기준이 되는 프로퍼티 (선택 사항)
Cache Name 성능 향상을 위한 캐시 파일 이름 (선택 사항)

4. 구현 단계 (Code Snippet)

Step 1: FRC 초기화

lazy var fetchedResultsController: NSFetchedResultsController<Item> = {
    let fetchRequest: NSFetchRequest<Item> = Item.fetchRequest()

    // FRC는 반드시 최소 하나 이상의 SortDescriptor가 필요합니다.
    let sort = NSSortDescriptor(key: "createdAt", ascending: false)
    fetchRequest.sortDescriptors = [sort]

    let controller = NSFetchedResultsController(
        fetchRequest: fetchRequest,
        managedObjectContext: self.container.viewContext,
        sectionNameKeyPath: nil, // 섹션 구분이 필요하면 해당 키값 입력
        cacheName: "itemsCache"
    )

    // Delegate 설정
    controller.delegate = self
    return controller
}()

Step 2: 데이터 가져오기 실행

do {
    try fetchedResultsController.performFetch()
} catch {
    print("Fetch failed: \(error)")
}

Step 3: NSFetchedResultsControllerDelegate 구현

데이터가 변경될 때 UI를 업데이트하는 로직입니다. (iOS 13+ 기준 DiffableDataSource를 쓰지 않는 전통적인 방식)

extension ViewController: NSFetchedResultsControllerDelegate {
    func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.beginUpdates()
    }

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anyObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
        switch type {
        case .insert:
            tableView.insertRows(at: [newIndexPath!], with: .fade)
        case .delete:
            tableView.deleteRows(at: [indexPath!], with: .fade)
        case .update:
            tableView.reloadRows(at: [indexPath!], with: .none)
        case .move:
            tableView.moveRow(at: indexPath!, to: newIndexPath!)
        @unknown default:
            break
        }
    }

    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        tableView.endUpdates()
    }
}

5. 결론: 언제 써야 할까?

  • 사용해야 할 때: Core Data의 엔티티가 테이블뷰와 1:1로 매칭되며, 데이터의 삽입/삭제가 빈번하게 일어날 때.
  • 사용하지 않아도 될 때: 데이터 양이 매우 적거나, Core Data가 아닌 단순 배열 데이터를 보여줄 때.

💡 Tip: iOS 13 이후부터는 UICollectionViewDiffableDataSource와 함께 FRC를 사용하여 더욱 매끄러운 애니메이션과 간결한 코드를 작성할 수 있습니다.

답글 남기기