YunDaeHyeon
← Lab

DiffableDataSource에 대해 알아보자

스터디2025.07.01
  • Swift
  • DiffableDataSource
  • Git

🤔 UITableViewDiffableDataSource?

UICollectionViewDiffableDataSource와의 차이점은 다음에 알아보자

iOS 13 이상부터는 UITableView와 UICollectionView에서 사용되는 DataSource를 더 안전하고 편리하게 관리할 수 있는 DiffableDataSource를 제공한다고 한다.

여기서 더 안전하고 편리하게가 무슨 의미일까?


전통적인 DataSource 방식의 문제점

WWDC19에서 제공되는 영상에 따르면 기존 개발자들은 아래와 같이 DataSource를 구성했다.

위 내용은 정말 익숙하다.

numberOfSections을 통해 섹션을 분리하고, numberOfItemsInSection을 통해 섹션 별 아이템의 개수를 지정한 뒤 cellForItemAt을 호출해 각각의 셀을 설정한다.

하지만 이 부분에서 고질적인 문제가 존재한다.

만약에 전체 데이터가 아니라 특정 데이터(혹은 셀) 변경된다면. 즉 DataSource를 업데이트 하기 위해서는 performBatchUpdates()나 beginUpdates(), endUpdates() 를 혼합해서 사용해야한다.

전체 데이터를 갱신해야한다면 reloadData()를 사용하겠지만 이는 데이터의 양이 방대할 경우 문제가 생긴다.

또 데이터의 양이 방대하거나 일부 Cell만 업데이트 하고 싶다면 performBatchUpdates()를 통해 개별 섹션이나 아이템을 업데이트 해야한다.

하지만 이 부분은 정말 복잡하고 오류가 발생하기 매우 쉬운 구조이다.

그러다보니 개발자들은 울면서 reloadData()를 사용하게 되니 애니메이션이 없는 부자연스러운 UI 업데이트가 탄생했다.

또한 cellForItemAt에서 사용되는 indexPath 자체도 결국 데이터의 위치를 나타내는 임시적인 값이다.

그러다보니 데이터가 추가되거나 삭제될 때마다 변경되는 요소가 존재한다.

이러한 문제점을 잡고자 탄생한 API가 바로 DiffableDataSource 이다.


DiffableDataSource의 해결책

위 사진은 WWDC19의 Advances in UI Data Sources에 나온 중요하다고 생각되는 부분을 캡쳐한 사진이다.

DiffableDataSource가 해결책으로 제시한 부분은 대표적으로 3가지가 존재한다.

  1. 스냅샷 기반 단일 진실
  • DiffableDataSource는 스냅샷을 통해 현재 UI의 상태를 하나의 진실로 관리한다.
  • Controller는 performBatchUpdates나 reload를 할 필요 없이 새로운 스냅샷을 생성시켜 apply() 시키면 된다!
  1. 자동 차이 계산
  • DiffableDataSource는 새로운 스냅샷과 기존 스냅샷을 자동으로 비교해준다.
  • 자동으로 비교가 되기 때문에 셀이 삽입되거나 삭제되거나 하는 작업이 발생되었을 때 자동으로 애니메이션과 함께 반영이 된다!
  1. 안정적인 식별자
  • DiffableDataSource는 전통적 DataSource에서 사용된 indexPath가 아닌 Hashable한 고유 식별자를 사용하여 데이터의 위치가 변경되도 쉽게 추적할 수 있게 해준다!

즉, Cell의 추가 및 삭제를 개발자가 직접 신경 쓸 필요가 없으며 Cell을 업데이트하기 위해선 스냅샷을 **적용(apply) 하면 끝난다.**


🤔 DiffableDataSource의 스냅샷이 뭐지?

스냅샷은 UI의 상태를 가지고 있는 객체이며 이를 통해 Cell 업데이트 시 IndexPath로 접근할 필요 없이 데이터를 업데이트 할 수 있다.

말 그대로 스냅샷은 데이터의 최종 상태를 기술한 객체이다.

쉽게 이해해보자면 사진을 찍듯이 지금 이 화면에는 이러한 데이터가 있어야한다.라고 찍어서 전달한다.

DiffableDataSource는 이전 사진(스냅샷) 과 새로운 사진(스냅샷) 을 비교(diff)해서 자동으로 입력, 삭제, 갱신 등을 수행해준다.

아래 이미지를 보면 이해하기 쉽다.

셀의 위치가 변경된 스냅샷(New Snapshot)을 기존 스냅샷에 apply() 시키면 자동으로 비교되어 Final Snapshot 이 생성된다.

마치 Git의 diff 개념으로 이해하면 쉽다.


DiffableDataSource의 정의 살펴보기

DiffableDataSource의 정의를 보면 다음과 같다.

DiffableDataSource는 전통적 DataSource에서 사용된 indexPath가 아닌 Hashable한 고유 식별자를 사용하여 데이터의 위치가 변경되도 쉽게 추적할 수 있게 한다는 걸 기억하자.

위 정의에서 볼 수 있듯이 모든 섹션과 아이템 식별자(SectionIdentifierType, ItemIdentifierType)는 Hashable 해야함을 알 수 있다.

class ViewController: UIViewController {

    @available(iOS 13.0, *)
    private lazy var diffableDataSource: UITableViewDiffableDataSource<Int, ViewModel> = {
        return UITableViewDiffableDataSource<Int, ViewModel>(tableView: logTableView,
                                                             cellProvider: { [weak self] (tableView, indexPath, itemIdentifier) in

            guard let cell = tableView.dequeueReusableCell(withIdentifier: "MyTableViewCell",
                                                           for: indexPath) as? MyTableViewCell,
                  let viewModel = self?.viewModel else {
                      return tableView.dequeueReusableCell(withIdentifier: "UITableViewCell",
                                                           for: indexPath)
                  }

            let cellViewModel = ViewModel(identifier: itemIdentifier)
            cell.viewModel = cellViewModel
            return cell
        })
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        if #available(iOS 13.0, *) {
            tableView.dataSource = diffableDataSource
        } else {
            tableView.dataSource = self
        }
    }
}

위 코드는 Diffable DataSource의 예시 코드이다.

이때 CellProvider 클로저는 기존 방식의 cellForItemAt() 을 대체하는 형태로 사용할 수 있다.

또한 내부적으로 UITableViewDataSource를 따르고 있기 때문에 기존 UITableViewDataSource 는 호출할 필요가 없다.

위처럼 정의한 데이터 소스에 새로운 데이터(스냅샷)을 추가하고 싶은 경우 NSDiffableDataSourceSnapshot을 생성한 뒤 추가하면 된다.

var snapshot = NSDiffableDataSourceSnapshot<Int, UUID>()
snapshot.appendSections([0])
snapshot.appendItems([UUID(), UUID(), UUID()])
dataSource.apply(snapshot)

이때 스냅샷의 인스턴스에 사용되는 섹션과 아이템은 Hashable 을 준수하도록 해야한다.


직접 만들어보기

1. 모델 정의하기

DiffableDataSource가 diff를 계산하기 위해선 섹션과 아이템이 Hashable 해야한다.

아이템과 섹션을 다음과 같이 정의한다.

struct Fruit: Hashable {
    let id = UUID()
    let name: String
}

enum Section {
    case main
}

2. ViewController 구현하기

import UIKit

class FruitListViewController: UIViewController {
    
    private var tableView: UITableView!
    private var dataSource: UITableViewDiffableDataSource<Section, Fruit>!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        title = "과일 리스트"
        view.backgroundColor = .systemBackground
        
        setupTableView()
        setupDataSource()
        applyInitialSnapshot()
    }
    
    private func setupTableView() {
        tableView = UITableView(frame: view.bounds, style: .plain)
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
        view.addSubview(tableView)
    }
}

UITableViewDiffableDataSource을 선언한 뒤 제네릭 타입에 구현한 커스텀 타입을 대입한다.

3. UITableViewDiffableDataSource 구현하기

    private func setupDataSource() {
        dataSource = UITableViewDiffableDataSource<Section, Fruit>(tableView: tableView) {
            (tableView, indexPath, fruit) -> UITableViewCell? in
            let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
            cell.textLabel?.text = fruit.name
            return cell
        }
    }

CellProvider는 기존 cellForItemAt 을 대체할 수 있으므로 위와 같이 정의할 수 있다.

4. Snapshot 정의하기

    private func applyInitialSnapshot() {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Fruit>()
        snapshot.appendSections([.main])
        snapshot.appendItems([
            Fruit(name: "사과"),
            Fruit(name: "바나나"),
            Fruit(name: "오렌지")
        ])
        dataSource.apply(snapshot, animatingDifferences: true)
    }

NSDiffableDataSourceSnapshot를 통해 DiffableDataSource에서 사용될 스냅샷을 생성해 현재의 UI 상태를 정의한다.

즉 현재 시점에선 과일 이름을 가진 3개의 셀이 존재하게 된다.

5. 아이템 추가하기

만약, 포도라는 새로운 아이템이 추가된다면 현재의 snapshot 객체를 끌고와 apply해주면 된다!

@objc private func addFruit() {
    var snapshot = dataSource.snapshot()
    snapshot.appendItems([Fruit(name: "🍇 포도")])
    dataSource.apply(snapshot, animatingDifferences: true)
}

이때 snapshot()이라는 메서드는 현재 데이터 소스의 상태를 복사해오는 메서드이다.

가져온 상태에 새로운 아이템을 append한 뒤 apply를 통해 갱신하면 된다!


⚡ DiffableDataSource의 참고 사항

만약 DiffableDataSource를 적용했다면 더 이상 performBatchUpdates()나 insertItems(), deleteItems() 등의 API를 사용해선 안된다.

스냅샷은 새로운 인스턴스나 기존 스냅샷을 활용해 생성할 수 있다.

모든 스냅샷의 업데이트(셀 혹은 데이터의 업데이트)는 API를 통해 진행할 수 있다.

기본적으로 제공되는 API는 다음과 같다.

성능 측면에서 DiffableDataSource의 diffing 알고리즘은 정말 빠르다.

이때 새로운 스냅샷을 적용하는 apply()의 경우 기존 API와 다르게 백그라운드 큐에서 동작하는게 안전하다.

즉, 항상 메인 스레드에서 사용될 필요가 없지만 ⭐ 항상 동일한 큐에서 apply()가 호출되어야 한다!


참고 및 출처

UITableViewDiffableDataSource | Apple Developer Documentation

NSDiffableDataSourceSnapshot | Apple Developer Documentation

Advances in UI Data Sources - WWDC19 - Videos - Apple Developer

DiffableDataSource로 안전하게 UIKit List 업데이트하기 - hcn1519 DEV

[iOS] DiffableDataSource 헷갈리는 것 정리

[iOS - swift] 1. Diffable Data Source - UITableViewDiffableDataSource (테이블 뷰)