
💡 지난 포스팅에서 기존 DataSource의 문제점에 기반해서 Diffable datasource가 등장한 배경에 대해 알아보았다.
이번 포스팅을 통해서 DiffableDataSource에 대해 알아보자!
기존 DataSocrce는 data 관리 및 UI 업데이트를 관리하는 객체가 분리되었기 때문에 다음과 같은 문제들이 발생할 가능성이 있었다.
1. 변경사항들을 Animation으로 표현해주기 어렵다.
기존 DataSource는 데이터의 추가, 삭제, 이동 등의 변화를 애니메이션으로 표현하는 것이 매우 까다로웠다. 복잡한 애니메이션 로직이 필요하기도 하고, 잘못 관리하게 되면 UI가 불안정해질 수 있는 가능성을 가지고 있었다.
2. Invalid batch updates detected의 발생으로 앱이 종료되는 현상이 있었다.
데이터와 UI 사이 동기화 문제로, 위 오류가 발생하는 경우가 존재했다. 앱이 종료되는 크리티컬한 문제로, 앱의 안정성과 UX를 저해하는 이슈로 이어진다.
3. IndexPath 기반으로 접근하기 때문에, bad access 가 발생할 가능성이 존재한다.
데이터가 업데이트 되기 전, 원본 데이터에 접근하게 되는 경우, bad access가 발생할 가능성이 존재한다.
이 문제를 해결하기 위해 Diffable DataSource는 data 관리 및 UI 업데이트 로직을 하나의 객체에서 처리할 수 있도록 변경되었다. 이제 DiffableDataSource에 대해 알아보자.
UI를 위한 Data를 분리하지 않는다고 했다. 그렇다면, dataSource가 어떻게 그 데이터들을 관리하는지에 대해 간단히 살펴보자.
우리는 이제 UI를 업데이트 시키기 위한 데이터는 viewController 또는 viewModel에 저장하지 않아도 된다. 데이터의 추가, 삭제, 수정 정보를 dataSource에게 알려주기만하면, UI가 알아서 업데이트 되기 때문이다.
그렇기 때문에 dataSource의 Item은 반드시 Hashable해야한다.
당연한 이야기겠지만, 조금 더 자세히 살펴보면 이해가 쉬워질 것 같다.
예를 들어, 데이터가 String 형태로 저장되어있다고 가정해보자.
private var items: [String] = ["data1", "data2", "data3"]
여기서 data3가 items에 추가된 상황을 생각해보자.
items = ["data1", "data2", "data3", "data3"]
기존 dataSource의 insertItems(at:)를 실행시키는 상황과 동일한 형태이다. 우리는 4번째에 "data4"가 추가되었다고 알려줬을 것이다.
하지만 DiffableDataSource는 더 이상 indexPath를 기반으로 연산하지 않는다. 변경된 데이터를 기반으로 애니메이션을 보여주는 것이 전부이다.
그런데 여기서 문제가 발생한다. 기존에 있던 data3을 data5라는 이름으로 변경하고 싶다는 것이다. diffableDataSource에게 "data4"을 "data5"로 바꿔! 라고 명령하면, 제대로 수행할 수 있을까? 그렇지 못하다는 것이다. 그렇기 때문에 Item은 반드시 Hashable해야한다.
즉, diffableDataSource 스스로 아이템의 변화를 인지하기 위해서는 반드시 Hashable 프로토콜 채택이 필요하다.
더불어, Hashable 프로토콜을 채택함으로써 DiffableDataSource는 데이터 항목의 고유성을 보장할 수 있다. 이를 통해, 데이터의 변경사항을 추적하고 업데이트하는 과정이 O(n) 시간이 아닌 O(1) 시간에 이루어질 수 있다는 것을 보장하게 된다.
Snapshot은 Diffable DataSource에서 데이터의 변경 사항을 추적하는 중요한 역할을 한다. 단어 그대로 dataSource에 저장된 데이터 상태를 '스냅샷' 혹은 사진 찍는 것처럼 복제하는 것을 의미한다.
var snapshot = dataSource.snapshot()
위 코드는 현재 dataSource에 상태를 snapshot객체에 저장하는 코드이다. 이 snapshot을 기반으로 데이터의 변경사항을 추적할 수 있게 된다.
// 데이터 가공
dataSource.apply(snapshot)
찍어둔 데이터 상태를 기반으로 데이터를 가공하고, 변경된 snapshot을 다시 dataSource에 적용하게 된다. 이를 통해 dataSource는 변경된 데이터 상태에 따라 UI를 자동으로 업데이트한다.
이와 같은 방식은 애니메이션을 생성할 때 특히 유용하다. 예를 들어, 서버에서 10개의 데이터를 받아오고 이를 하나씩 추가한다고 가정해보자. 이 경우, 각 데이터가 추가될 때마다 애니메이션을 생성하는 것이 아니라, 10개의 데이터가 모두 추가된 후 한 번에 애니메이션을 생성하는 것이 더 효율적이다. 이처럼, Snapshot은 애니메이션을 생성하는 '단위'를 관리하는 데 도움이 된다.
우선 dataSource는 ViewController 모든 곳에서 접근 가능해야한다. 이를 위해 viewController 내부에 dataSource 원형을 선언해준다.
원형만 선언해둔 상태이기 때문에, 반드시 최초 접근 전에는 dataSource 초기화를 해 주어야한다. 이때, cell Provider를 통해 dataSource를 생성하게 된다.
dataSource 생성자
@MainActor
init(
collectionView: UICollectionView,
cellProvider: @escaping UICollectionViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType>.CellProvider
)
ItemIdentifierType은 DataSource에 사용되는 아이템 타입을 의미한다. 이를 실제 코드에 적용해보면 다음과 같다.private var dataSource: **UICollectionViewDiffableDataSource**<Section, Int>!
private func setupDataSource() {
dataSource = UICollectionViewDiffableDataSource<Section, Int>(collectionView: collectionView) { collectionView, indexPath, item in
guard let cell = collectionView.dequeueReusableCell(withIdentifier: MyCollectionviewCell.id, for: indexPath) as? MyCollectionviewCell else { return UICollectionViewCell() }
cell.config(with: item)
return cell
}
위의 코드에서, UICollectionViewDiffableDataSource<Section, Int>를 생성하고 있다. 이는 Section 타입이 Section, item 타입이 Int인 DiffableDataSource를 의미합니다. 이후에 CellProvider 클로저를 통해 각 IndexPath에 어떤 셀을 표시할지 지정해준다.
dequeueReusableCell 메소드를 통해 재사용 가능한 셀을 가져오고, config(with: item)을 통해 셀의 내용을 설정한 후에 셀을 반환한다. 이 과정이 모든 IndexPath에 대해 반복되며, 이를 통해 collectionView의 모든 셀이 설정된다.
데이터에 변경이 발생했을 때, Snapshot을 사용할 수 있다. 이는 UI 업데이트를 위한 기준으로, UI가 변경될 때마다 적절한 Snapshot을 적용하는 과정이다.
var snapshot = dataSource.snapshot()
snapshot.appendSections([.main])
snapshot.appendItems(newData, toSection: .main)
dataSource.apply(snapshot)
위의 코드에서, 먼저 dataSource.snapshot()을 통해 현재 dataSource의 Snapshot을 가져온다. 그 다음, appendSections([.main])을 통해 .main 섹션을 snapshot에 추가합니다.
그리고 나서, appendItems(newData, toSection: .main)을 통해 새로운 데이터(newData)를 .main 섹션에 추가한다. 마지막으로, dataSource.apply(snapshot)을 통해 변경된 snapshot을 dataSource에 적용한다.
이렇게 하면, 데이터가 추가될 때마다 애니메이션이 자동으로 생성되어 UI가 업데이트된다. 이는 Diffable DataSource의 가장 큰 장점 중 하나로, 복잡한 애니메이션 로직 없이도 자연스러운 UI 업데이트를 구현할 수 있다.
위 방법과 동일하지만, appendSection하는 과정을 생략한다.
var snapshot = dataSource.snapshot()
snapshot.appendItems(newData, toSection: .main)
dataSource.apply(snapshot)
Updating Collection Views Using Diffable Data Sources | Apple Developer Documentation