- Diffable DataSource2025년 03월 09일
- 2료일
- 작성자
- 2025.03.09.:29
와… 진짜 오랜만에 UIKit 관련 공부를 진행하는 것 같네요. 유킷으로 개발한 지가 좀 됐지만, 그래도 이건 알아야 하는 중요한 지식이라 정리 시작해볼게요! 오늘은 Diffable DataSource에 대해 자세히 다뤄보고, 이 내용을 바탕으로 블로그 글을 완성해보겠습니다.등장배경
기존에는 테이블이나 콜렉션 내부의 데이터를 사용하거나 변경할때 indexPath를 통해 Cell 을 가져오고 데이터를 변화시켜 주었다. 또한 데이터 업데이트로 다시 뷰를 그릴때는 reloadData() or performBatchUpdates()를 호출해주었습니다.
// 기존 방식 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return items.count } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) cell.textLabel?.text = items[indexPath.row] return cell } // 데이터 업데이트 시 items = newItems tableView.reloadData() // 전체 테이블 다시 로드
- reloadData()는 테이블 뷰나 컬렉션 뷰의 모든 셀을 한 번에 다시 로드하는 가장 간단한 방법입니다.
- 사용시기: 데이터 소스가 완전히 변경되었을 때/ 여러섹션이나 많은 행에 걸쳐 광범위한 변경이 있을 때/ 간단하게 구현하고 싶을때 (애니메이션이 중요하지 않은경우)// 초기 데이터 로드시/
- 장점: 구현이 간닪나고 데이터 소스와UI상태 간 불일치 걱정 없음
- 단점: 애니메이션 없이 화면이 깜빡임. 변경된 부분만 갱신하는것이 아닌 전체를 다시 로드하므로 성능 비효율적. 스크롤 위치가 초기화 될수 있습니다.
tableView.performBatchUpdates({ // 여기서 데이터 소스를 먼저 업데이트 self.dataSource.append(newItems) // 그 다음 테이블 뷰에 변경 사항을 알림 let newIndexPaths = (oldCount..<self.dataSource.count).map { IndexPath(row: $0, section: 0) } tableView.insertRows(at: newIndexPaths, with: .automatic) }) { completed in // 업데이트 완료 후 실행할 코드 } // UITableView의 경우 아래 방식도 사용 가능 tableView.beginUpdates() // 여기서 데이터 소스와 테이블 뷰 업데이트 tableView.endUpdates()
- performBatchUpdates(): 테이블 뷰나 컬렉션뷰의 특정 부분만 업데이트하면서 애니메이션 효과를 적용할 수 있는 방법입니다.
- 사용시기: 데이터의 일부분만 변경될때/ 변경사항에 애니메이션 주고 싶을때/ 사용자경험을 향상시키고 성능 최적화가 필요할때
- 장점: 부드러운 UI업데이트 가능/ 변경된 부분만 업데이트하므로 성능 효율적/ 스크롤 위치 유지 가능
- 단점: 구현이 복잡함/ 데이터소스와 UI상태를 정확히 동기화해야함/ 동기화가 잘모소디면 index out of range등의 크래시 발생 위험
- 주의 사항: 항상 데이터 소스를 먼저 업데이트 한후 UI 업데이트 명령을 호출해야하고 추가 삭제 이동하는 항목의 인덱스 경로가 정확해야합니다. 또한 중첩된 batch Updates는 피하고 비동기 작업일 경우 타이밍 이슈에 특히 주의해야합니다.
이러한 복잡성과 오류 가능성 때문에 Diffable DataSource가 나왔습니다
DiffableDataSource란?
1. 식별자 기반 데이터 관리:
- 각 항목은 고유 식별자(Hashable)를 통해 관리됩니다.
- IndexPath 대신 식별자로 항목을 참조하므로 데이터와 UI의 동기화 문제가 해결됩니다.
2. 스냅샷 기반 업데이트:
- 현재 UI상태와 원하는 새 상태를 스냅샷으로 비교합니다.
- 두 상태 간의 차이점만 계산하여 효율적으로 업데이트합니다.
- 이전 상태를 완전히 대체하는 선언적 접근 방식입니다.
3. 자동 차이점 계산(Diffing):
- 프레임워크가 자동으로 이전 스냅샷과 새 스냅샷간의 차이를 계산합니다.
- 어떤 항목이 추가. 삭제 이동 되었는지를 자동으로 파악합니다
4. 쓰레드 안정성: 백그라운드 쓰레드에서 데이터 처리하고 메인쓰레드에서 UI 업데이트
- 비동기 데이터 로딩과 UI업데이트의 안전한 분리 가능해집니다.
쉽게 말해, Diffable DataSource는 데이터 타입을 정의하고 스냅샷을 만들어 뷰에 반영하는 방식이에요. 데이터가 바뀌면 새로운 스냅샷을 생성해서 적용하고, 이전 스냅샷과 비교해 달라진 부분만 부드럽게 업데이트해줍니다. 특히 애니메이션을 자동으로 지원해서 UI 전환이 자연스러워요. 이제 indexPath를 직접 다룰 필요 없이 고유 식별자를 통해 데이터를 배치할 수 있게 됐습니다.
구현 단계
1. Section과 Item 식별자 정의
// 섹션 정의 enum Section: Int, CaseIterable, Hashable { case featured case popular case recent } // 항목 정의 struct Item: Hashable { let id = UUID() // 고유 식별자 var title: String var description: String // 고유성은 id로만 판단 (내용이 바뀌어도 같은 항목으로 취급) func hash(into hasher: inout Hasher) { hasher.combine(id) } static func == (lhs: Item, rhs: Item) -> Bool { return lhs.id == rhs.id } }
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>! private func configureDataSource() { dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { (collectionView, indexPath, item) -> UICollectionViewCell? in let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! MyCell cell.configure(with: item) return cell } }
CellRegistration을 통해 커스텀 셀을 컬렉션 뷰에 등록할 수 있다.
SupplementaryRegistration을 통해 커스텀 뷰를 컬렉션 뷰에 등록할 수 있다.
2. 초기 스냅샷을 만들고 DiffableDataSource에 적용
NSDiffableDataSourceSnapshot: 특정 시점의 뷰에서 데이터 상태를 표현.
preconcurrency struct NSDiffableDataSourceSnapshot<SectionIdentifierType, ItemIdentifierType> where SectionIdentifierType : Hashable, SectionIdentifierType : Sendable, ItemIdentifierType : Hashable, ItemIdentifierType : Sendable
스냅샷을 사용하여 뷰에 표시되는 데이터의 초기 상태 설정하고 스냅샷을 사용하여 뷰에 표시되는 데이터에 대한 변경 사항을 반영합니다.
Section,Item 들은 고유한 identifier로 구별해야하기에 Hashable프로토콜을 채택해야한다.
func updateUI() { // 새 스냅샷 생성 var snapshot = NSDiffableDataSourceSnapshot<Section, Item>() // 모든 섹션 추가 snapshot.appendSections(Section.allCases) // 각 섹션에 항목 추가 snapshot.appendItems(featuredItems, toSection: .featured) snapshot.appendItems(popularItems, toSection: .popular) snapshot.appendItems(recentItems, toSection: .recent) // 스냅샷 적용 (애니메이션 효과 포함) dataSource.apply(snapshot, animatingDifferences: true) }
애플 공식문서 코드이다. 빈 스냅샷을 생성하고 세개의 항목이 있는 단일섹션으로 채웠다. 그후 apply하여 이전 상태와 새 상태 사이의 ui업데이트를 애니메이션화했다.
고급 기능
섹션 헤더/ 푸터 추가
// 헤더 등록 (iOS 14+) let headerRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { (headerView, elementKind, indexPath) in var configuration = headerView.defaultContentConfiguration() configuration.text = "섹션 헤더" headerView.contentConfiguration = configuration } // 데이터 소스에 supplementaryViewProvider 설정 dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in if kind == UICollectionView.elementKindSectionHeader { return collectionView.dequeueConfiguredReusableSupplementary(using: headerRegistration, for: indexPath) } return nil }
주요장점
1. 선언적 UI업데이트: 뷰의 원하는 상태를 선언하면 프레임워크가 현재 상태에서 원하는 상태로 전환하는 방법을 계산함으로 복잡한 insert/delete/move 로직을 직접 작성할 필요가 없다.
2. 일관성 보장: indexPath가 아닌 고유 식별자로 항목을 참조하므로 데이터 UI 불일치 방지할수 있다.
3. 자동 애니메이션
4. 변경된 부분만 효육적으로 업데이트
5. 코드 가독성 향상: 데이터 소스 로직과 UI업데이트 로직이 명확히 분리
사용 시나리오
1. 동적 컨텐츠: 사용자 검색 결과 표시하거나 실시간 업데이트가 필요한 피드, 정렬/필터링 기능이 있는 리스트
2. 복잡한 섹션 구조: 여러 유형의 콘텐츠를 섹션으로 구분하는 UI, 동적으로 추가/제거되는 UI
3. 비동기 데이터로딩: 네트워크 요청으로 데이터를 가져오는 경우
참고자료
https://applecider2020.tistory.com/37
[CollectionView] Diffable DataSource 이해하기 (1/3) - Advances in UI Data Sources (WWDC19)
CollectionView / TableView와 관련해서 "Diffable DataSource" 개념이 등장했다. 러닝커브가 조금 있는 내용이라 포스팅을 남기려고 한다. ✏️ 새로운 기술을 습득하기 가장 좋은 방법은 Apple이 만든 WWDC 영
applecider2020.tistory.com
'UIkit' 카테고리의 다른 글
Coordinator Pattern - UIKit (1) 2024.05.14 다음글이전글이전 글이 없습니다.댓글