RxDataSource의 다중 섹션을 사용할 때 ReloadData 문제와 해결방법

서론

  • 해당 프로젝트는 RxSwift, ReactorKit 기반으로 구현되어 있어서 다른 기술 스택 모두 Reactive화를 시키는 것에 의의를 두고 RxDataSource를 도입했습니다.
  • 하지만 RxDataSource를 사용하면서 다중 에서의 트러블 슈팅에 대한 것을 소개하고 그 해결 과정에 대해서 소개하려고 합니다.

 

RxDataSource의 문제 발견

  • 마지막 취향 섹션을 선택할 때, 나머지 이름, ID 섹션이 모두 초기화되는 문제점이 발생했습니다.
  • 왜 이런 이슈가 발생하는걸까요? 원인을 파악하기 위해서 디버그 과정을 거쳐야합니다.
  private lazy var dataSource: EditMyPageDataSource = EditMyPageDataSource(
    configureCell: { [weak self] _, collectionView, indexPath, item in
      switch item {
      case let .name(name, placeholder):
        let cell = collectionView.dequeueReusableCell(for: indexPath) as FavorTextFieldCell
        cell.bind(placeholder: placeholder)
        cell.bind(text: name)
        print(cell)
        return cell
      case let .id(name, placeholder):
        let cell = collectionView.dequeueReusableCell(for: indexPath) as FavorTextFieldCell
        cell.bind(placeholder: placeholder)
        cell.bind(text: name)
        print(cell)
        return cell
      case let .favor(isSelected, favor):
        let cell = collectionView.dequeueReusableCell(for: indexPath) as EditMyPagePreferenceCell
        cell.isButtonSelected = isSelected
        cell.favor = favor
        return cell
      }
)
  • 여기서 EditMyPageDataSource는 Cell들을 Dequeue를 시키는 configureCell이라는 클로저를 갖고 있습니다.
  • 마지막 .favor 섹션이 변경될 때, 모든 섹션들의 셀들이 다시 정의되고 있는 것이 원인입니다.
open class RxCollectionViewSectionedReloadDataSource<Section: SectionModelType>
    : CollectionViewSectionedDataSource<Section>
    , RxCollectionViewDataSourceType {
    public typealias Element = [Section]

    open func collectionView(_ collectionView: UICollectionView, observedEvent: Event<Element>) {
        Binder(self) { dataSource, element in
            #if DEBUG
                dataSource._dataSourceBound = true
            #endif
            dataSource.setSections(element)
            collectionView.reloadData() ❗️❗️
            collectionView.collectionViewLayout.invalidateLayout()
        }.on(observedEvent)
    }
}
  • RxDataSource의 소스코드를 살펴보니, DataSource에 보내준 CollectionView에 대해서 값이 변경되면 무조건적으로 reloadData를 호출하고 있는 것이 원인이었습니다.

 

해결 방법

  • 일반적으로 CollectionView에는 세 가지의 reload메서드가 존재합니다.
    • 모든 값을 Reload하는 reloadData()
    • 특정 섹션을 Reload하는 reloadSections()
    • 특정 아이템을 Reload하는 reloadItem(at:)
  • 해결 방법은 두 가지로 나뉠 수 있습니다.
    • Custom RxDataSource
    • RxDataSource 삭제하고 DiffableDataSource 사용

Custom RxDataSource

  • 앞서 살펴본 collectionView라는 메서드를 override를 해서 재정의를 할 수 있습니다.
class FavorDataSource<Section: SectionModelType>: RxCollectionViewSectionedReloadDataSource<Section> {
  override func collectionView(_ collectionView: UICollectionView, observedEvent: Event<[Section]>) {
    Binder(self) { dataSource, element in
      dataSource.setSections(element)
      collectionView.reloadSections([2], animationStyle: .none) 🟢
      collectionView.collectionViewLayout.invalidateLayout()
    }.on(observedEvent)
  }
}

  • 취향 섹션은 2번 섹션이기 때문에, reloadSections를 통해 해당 섹션만 업데이트가 되도록 구현했습니다.
  • 하지만 여기에는 꽤나 큰 문제점이 있습니다.
  • UI는 언제 어떻게 업데이트 되는지 경우의 수가 너무 많은 것이 문제점입니다. 지금은 비교적 간편한 UI이지만, UI가 더 복잡해지면 스파게티 코드가 될 가능성이 큽니다.
  • 결론적으로 이 방법은 어떻게든 구현할 수는 있지만, 복잡성이 요구되고 유지 보수성이 증가할 것으로 우려되어서 이 방법을 채택하지 않았습니다. 

RxDataSource 삭제하고 DiffableDataSource 사용

  • 이 방법은 기존 RxDataSource를 삭제하고 Native에서 제공하는 DiffableDataSource를 사용하는 방법입니다.
  • RxDataSource의 가장 큰 장점은 bind(to:)를 통해 Rx화를 간편하게 시켜줄 수 있는 장점이 있습니다.
  • 하지만 이 장점은 코드 몇줄이 간결해지는 장점빼고는 모든 것에서 Native의 장점을 가져가는 편이 낫다고 판단했습니다.
  • RxDataSource와는 달리 원하는 시점에 원하는 Item, Section을 자유자재로 업데이트가 가능합니다.
public protocol SectionModelType: Hashable { }
public protocol SectionModelItem: Hashable { }

enum ProfileSectionItem: SectionModelItem {
  case profileSetupHelper(ProfileSetupHelperCellReactor)
  case preferences(ProfilePreferenceCellReactor)
  case anniversaries(ProfileAnniversaryCellReactor)
  case memo
  case friends(ProfileFriendCellReactor)
}

enum ProfileSection: SectionModelType {
  case profileSetupHelper
  case preferences
  case anniversaries
  case memo
  case friends
}
  • 이처럼 DiffableDataSource는 데이터 값에 무조건적으로 Hashable을 채택해야합니다.
  • Hash값으로 데이터의 변경을 판단하기 떄문입니다.
reactor.state.map { (sections: $0.sections, items: $0.items) }
  .asDriver(onErrorRecover: { _ in return .empty()})
  .drive(with: self, onNext: { owner, sectionData in
	var snapshot: NSDiffableDataSourceSnapshot<ProfileSection, ProfileSectionItem> = .init()
	snapshot.appendSections(sectionData.sections)
	sectionData.items.enumerated().forEach { idx, items in
	  snapshot.appendItems(items, toSection: sectionData.sections[idx])
	}
	owner.dataSource.apply(snapshot, animatingDifferences: false)
  })
  .disposed(by: self.disposeBag)
  • ViewModel에서 정의해준 Sections와 Items를 ViewController에 바인딩 하는 방식만 달리해주면 됩니다.
  • DiffableDataSource API인 snapshot을 통해 appendSection, appendItems를 통해 DataSource를 업데이트 시켜줍니다.
  • 결론적으로 데이터 값이 변경됨에 따라서 특정 섹션 및 아이템만 reload됩니다.

 

마치며

  • 이번 시간에는 RxDataSource의 다중 섹션에서의 문제점을 두 가지 해결방안으로 해결한 과정을 보여드렸습니다. 페이버 팀은 ReactiveX에 매료되어 모든 프로젝트를 Rx화를 시키려는 욕심에 이런 문제 해결 과정을 봉착했습니다.
  • 그렇지만 RxDataSource가 너무 단점만 있는 라이브러리는 아닙니다. RxDataSource는 단일 섹션에 굉장히 유리한 조건을 가지고 있습니다.
  • 따라서 상황 및 맥락에 따라서 어떤 API를 사용하는 것이 최선의 판단인지 따져보는 것이 중요하게 느낀 과정이었습니다.

 

Reference