Published on

๐Ÿ“ฑ iOS - RxDataSources์™€ reloadData

Authors
  • avatar
    Name
    ์ด์ฐฝ์ค€
    Twitter

๐Ÿ˜ต DataSource๋ฅผ reload ํ•˜๋Š” ๊ณผ์ •์—์„œ ๋ฌธ์ œ ๋ฐœ์ƒ

MyPage๋ฅผ ํŽธ์ง‘ํ•˜๋Š” View๋ฅผ ๊ตฌํ˜„ํ•˜๋˜ ์ค‘ ํ•˜๋‚˜์˜ Section ๊ฐ’์ด ์—…๋ฐ์ดํŠธ ๋˜๋ฉด ๋‹ค๋ฅธ Section์˜ ๊ฐ’์ด ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ ๋Œ์•„๊ฐ€๋Š” ํ˜„์ƒ์„ ๋งˆ์ฃผํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ•ด๋‹น View๋Š” ํ•˜๋‚˜์˜ UICollectionView๋กœ ๊ตฌ์„ฑ๋˜์—ˆ์œผ๋ฉฐ ์ตœ์ƒ๋‹จ์˜ ํ”„๋กœํ•„/๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง€ ๋ณ€๊ฒฝ ๋ถ€๋ถ„์€ Header, ์ด๋ฆ„/ID/์ทจํ–ฅ ๋ถ€๋ถ„์€ Section์œผ๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

  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)
        return cell
      case let .id(name, placeholder):
        let cell = collectionView.dequeueReusableCell(for: indexPath) as FavorTextFieldCell
        cell.bind(placeholder: placeholder)
        cell.bind(text: name)
        return cell
      case let .favor(isSelected, favor):
        let cell = collectionView.dequeueReusableCell(for: indexPath) as EditMyPagePreferenceCell
        cell.isButtonSelected = isSelected
        cell.favor = favor
        return cell
      }
    }, configureSupplementaryView: { dataSource, collectionView, kind, indexPath in
      switch kind {
      case EditMyPageCollectionHeaderView.reuseIdentifier:
        let header = collectionView.dequeueReusableSupplementaryView(
          ofKind: kind,
          for: indexPath
        ) as EditMyPageCollectionHeaderView
        return header
      case UICollectionView.elementKindSectionHeader:
        let header = collectionView.dequeueReusableSupplementaryView(
          ofKind: kind,
          for: indexPath
        ) as FavorSectionHeaderView
        let headerTitle = dataSource[indexPath.section].header
        header.updateTitle(headerTitle)
        return header
      case UICollectionView.elementKindSectionFooter:
        let footer = collectionView.dequeueReusableSupplementaryView(
          ofKind: kind,
          for: indexPath
        ) as FavorSectionFooterView
        footer.footerDescription = dataSource[indexPath.section].footer
        return footer
      default:
        return UICollectionReusableView()
      }
    }
  )

DataSource๋ฅผ ๋ฐ”์ธ๋”ฉํ•˜๋Š” ๊ณผ์ •์€ Rx์™€์˜ ํ˜ธํ™˜์„ฑ์„ ๊ณ ๋ คํ•˜์—ฌ RxDataSources ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ถ”๊ฐ€์ ์œผ๋กœ cell์„ dequeueํ•˜๊ณ  identifier๋ฅผ ์ •์˜ํ•˜๋Š” ๊ณผ์ •์„ ํŽธ๋ฆฌํ•˜๊ฒŒ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด Reusable ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

โ“ ์›์ธ ํŒŒ์•…

๊ฐ Cell์— rx๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ”์ธ๋”ฉํ•˜๋Š” ๊ณผ์ •์—์„œ์˜ ๋ฌธ์ œ, Reactor์˜ transform์—์„œ newState๊ฐ€ ์ƒˆ๋กญ๊ฒŒ ์ƒ์„ฑ๋˜๋Š” ๋ฌธ์ œ ๋“ฑ์„ ๊ณ ๋ คํ–ˆ์ง€๋งŒ ๊ฒฐ๊ตญ์—๋Š” ๋ชจ๋‘ ์•„๋‹ˆ์˜€์Šต๋‹ˆ๋‹ค.

์›์ธ์€ dataSource์— print๋ฌธ์„ ๋„ฃ์€ ํ›„์— ๋ฐœ๊ฒฌํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

  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
      }

๊ฐ Section์— ํ•˜๋‚˜์˜ Item๋งŒ ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฏ€๋กœ ์ทจํ–ฅ Section์ด ๋ณ€๊ฒฝ๋ ๋•Œ๋งˆ๋‹ค ๋ชจ๋“  Section์˜ dataSource๊ฐ€ ๋‹ค์‹œ ์ •์˜๋˜๊ณ  ์žˆ์—ˆ๋˜ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๋ชจ๋“  Section์˜ ๋ฐ์ดํ„ฐ๊ฐ€ reload๋˜๊ณ  ์žˆ๋‹ค๋Š” ์˜์‹ฌ์„ ํ’ˆ๊ณ  RxDataSources ๋‚ด๋ถ€ ์ฝ”๋“œ๋ฅผ ํŒŒ์•…ํ•ด๋ณด๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค.

โ— ์›์ธ ๋ฐœ๊ฒฌ

๋ฌธ์ œ๋Š” RxDataSources๊ฐ€ ๋งž์•˜์Šต๋‹ˆ๋‹ค.

์ผ๋‹จ ์ €ํฌ๊ฐ€ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋˜ RxCollectionViewSectionedReloadDataSource์˜ ๊ฒฝ์šฐ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

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)
    }
}

UIKit์€ UICollectionView์˜ ๊ฐ’์„ reloadํ•˜๋Š” ๋ฐฉ๋ฒ•์œผ๋กœ ํฌ๊ฒŒ

  1. ๋ชจ๋“  ๊ฐ’์„ reloadํ•˜๋Š” reloadData()
  2. ํŠน์ • Section๋งŒ์„ reloadํ•˜๋Š” reloadSections()
  3. ๋งˆ์ง€๋ง‰์œผ๋กœ ํŠน์ • Item๋งŒ์„ reloadํ•˜๋Š” reloadItem(at:)

์˜ ์„ธ๊ฐ€์ง€ ์ข…๋ฅ˜ API๋ฅผ ์ œ๊ณตํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ RxDataSources์˜ ๊ฒฝ์šฐ, ๋ฌด์กฐ๊ฑด reloadData()๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๋ชจ๋“  ๊ฐ’์„ reloadํ•˜๊ณ  ์žˆ๋Š” ์ ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

ChatGPT์—๊ฒŒ ๋ฌผ์–ด๋ณด๋‹ˆ RxDataSources๋Š” ๊ทœ๋ชจ๊ฐ€ ์ž‘์€ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค๋ฃฐ ๋•Œ ์‚ฌ์šฉ๋˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•˜์—ฌ ์„ค๊ณ„๋˜์—ˆ๊ณ , ๋ชจ๋“  ๊ฐ’์„ reloadํ•˜๋Š” ๊ฒƒ์ด ๋‹ค๋ฅธ trade-off์™€ ๋น„๊ตํ–ˆ์„ ๋•Œ ํšจ์œจ์ ์œผ๋กœ ํŒ๋‹จํ–ˆ๋‹ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

์ •ํ™•ํ•œ ์ •๋ณด์ธ์ง€ ๊ฐœ์ธ์ ์œผ๋กœ ์ถœ์ฒ˜๋ฅผ ์ฐพ์•„๋ณด๋ ค ํ–ˆ์ง€๋งŒ ์ฐพ์•„๋ณผ ์ˆ˜๋Š” ์—†์—ˆ์Šต๋‹ˆ๋‹ค..

์ œ๋ณด ๋ฐ›์Šต๋‹ˆ๋‹ค.. ๐Ÿ˜…

.name๊ณผ .id ์„น์…˜์˜ ๊ฒฝ์šฐ์—๋Š” View๊ฐ€ ๋กœ๋“œ๋์„ ๋•Œ ๋‹จ ํ•œ ๋ฒˆ๋งŒ ๋ฐ์ดํ„ฐ๊ฐ€ ๋กœ๋“œ๋˜๊ธฐ ๋•Œ๋ฌธ์— ๋ชจ๋“  Section์ด reload๋˜๋Š” ๊ฒฝ์šฐ ์ดˆ๊ธฐ๊ฐ’์œผ๋กœ ๋Œ์•„๊ฐ€๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

ํ˜น์‹œ๋‚˜ RxCollectionViewSectionedAnimatedDataSource๋Š” ๋ณ€๊ฒฝ๋˜๋Š” Section์„ ๊ณจ๋ผ๋‚ด์„œ ๋ถ€๋ถ„์ ์œผ๋กœ reload๊ฐ€ ๋˜๋‚˜ ์‹ถ์—ˆ์ง€๋งŒ.. ๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ฒ˜๋ฆฌ๋งŒ ๋ถ€๋ถ„์ ์œผ๋กœ ์ฒ˜๋ฆฌ๋˜๊ณ  reload๋Š” ์ „์ฒด์ ์œผ๋กœ ๋˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค..

โ€ผ๏ธ ํ•ด๊ฒฐ

์ด๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ๋‘ ๊ฐ€์ง€ ํ•ด๊ฒฐ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๋‹ค.

  1. Custom DataSource ํƒ€์ž…์„ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ•œ๋‹ค.
  2. RxDataSources๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค.

Custom DataSource ์‚ฌ์šฉ

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) // ๐Ÿ‘ˆ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ๊ณ ์ •์ ์œผ๋กœ 2๋ฒˆ์งธ ์„น์…˜๋งŒ reload๋˜๋„๋ก
      collectionView.collectionViewLayout.invalidateLayout()
    }.on(observedEvent)
  }
}

์ฒซ ๋ฒˆ์งธ ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

์œ„์™€ ๊ฐ™์ด Custom DataSource๋ฅผ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

reloadData() ๋Œ€์‹  reloadSections๋‚˜ reloadItem(at:)์„ ์‚ฌ์šฉํ•˜๋Š” DataSource๋ฅผ ๋”ฐ๋กœ ๋งŒ๋“ค์–ด ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ์šฐ์„  2๋ฒˆ์งธ Section์˜ ๊ฐ’๋“ค๋งŒ reload ๋˜๋„๋ก ๊ตฌํ˜„ํ•ด๋ณด์•˜์Šต๋‹ˆ๋‹ค.

Section์ด ๊นœ๋นก์ด๊ธด ํ•˜์ง€๋งŒ ์ผ๋‹จ ์›ํ•˜๋Š”๋Œ€๋กœ ๋™์ž‘์€ ํ•˜๋Š”๊ตฐ์š”..

๋‹จ์ ์€ Section์˜ ํ˜•ํƒœ์— ๋”ฐ๋ผ DataSource๋ฅผ View์— ๋งž๊ฒŒ ๊ฐ๊ฐ ๋งŒ๋“ค์–ด์„œ ์‚ฌ์šฉํ•ด์•ผ ํ•  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๋‹ค๋Š” ์ ์ด์˜€์Šต๋‹ˆ๋‹ค.

โ€œ์–ธ์ œ ์–ด๋–ค Section์„ reloadํ•ด์•ผํ•˜๋Š”๊ฐ€?โ€๋ฅผ Genericํ•˜๊ฒŒ ๊ณ„์‚ฐํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค๋Š” ์ ์€ ์ผ๋ฐ˜์ ์ธ ๊ฒฝ์šฐ์—์„œ๋„ ๊ฝค๋‚˜ ๋ณต์žกํ•œ ๋กœ์ง์„ ํ•„์š”๋กœ ํ•˜๊ณ , ์ถ”ํ›„ ๊ณผ์ •์—์„œ ์ŠคํŒŒ๊ฒŒํ‹ฐ ์ฝ”๋“œ๊ฐ€ ๋  ๊ฒƒ์ด๋ผ๋Š” ์ƒ๊ฐ์ด ๋“ค์–ด ๋ถ€์ ์ ˆํ•˜๋‹ค๊ณ  ํŒ๋‹จํ•˜์˜€์Šต๋‹ˆ๋‹ค.

RxDataSources ๋Œ€์‹  DiffableDataSources ์‚ฌ์šฉ

์‚ฌ์‹ค ๊ฐ€์žฅ ์ข‹์€ ๋ฐฉ๋ฒ•์€ Native ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด๊ฒ ์ฃ ?

์ €๋Š” DiffableDataSources์— ๋Œ€ํ•œ ์ถฉ๋ถ„ํ•œ ๊ณต๋ถ€๋ฅผ ํ•œ ๋’ค ์ €ํฌ ํ”„๋กœ์ ํŠธ์˜ RxData Source๋ฅผ DiffableDataSources๋กœ ๋Œ€์ฒดํ•ด๋„ ๋ฌธ์ œ๊ฐ€ ์—†๊ฒ ๋‹ค๋Š” ํŒ๋‹จ์„ ํ•˜๊ณ  RxDataSources๋ฅผ ๊ฑท์–ด๋‚ด๊ธฐ๋กœ ๊ฒฐ์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

Rx๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด์„œ๋„ DiffableDataSources๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐ์—๋Š” ์•„๋ฌด ๋ฌธ์ œ๊ฐ€ ์—†์„ ๊ฒƒ ๊ฐ™๋”๋ผ๊ณ ์š”..

bind(to:)๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ„ํŽธํ•˜๊ฒŒ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ”์ธ๋”ฉํ•  ์ˆ˜ ์žˆ๋‹ค๋ผ๋Š” ์žฅ์ ๋งŒ ํฌ๊ธฐํ•œ๋‹ค๋ฉด RxDataSources๋ฅผ ๊ฑท์–ด๋‚ด๋Š” ๋ฐ์—๋Š” ์•„๋ฌด ๋ฌธ์ œ๊ฐ€ ์—†์—ˆ์Šต๋‹ˆ๋‹ค.

์‹ฌ์ง€์–ด API๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํ˜•ํƒœ๋„ ์ƒ๋‹นํžˆ ์œ ์‚ฌํ•ด์„œ ๋Ÿฌ๋‹ ํ—ˆ๋“ค๋„ ๋‚ฎ์€ ํŽธ์— ์†ํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์ œ ์ƒํ™ฉ์—์„œ์˜ DiffableDataSources๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฐ€์žฅ ํฐ ์ด์ ์€ ์ œ๊ฐ€ ์›ํ•˜๋Š” ์‹œ์ ์— ์ œ๊ฐ€ ์›ํ•˜๋Š” Section์ด๋‚˜ Item๋งŒ์„ ์—…๋ฐ์ดํŠธ ํ•ด์ค„ ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ์ด์˜€์Šต๋‹ˆ๋‹ค.

์ž ์ด์ œ ๋ฆฌํŒฉํ† ๋ง์„ ํ•ด๋ด…์‹œ๋‹ค..! ๐Ÿ”ฅ

1. Section ์ •์˜

์šฐ์„  ๋‘ ๊ฐ€์ง€ ํ”„๋กœํ† ์ฝœ์„ ์ •์˜ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

public protocol SectionModelType: Hashable { }
public protocol SectionModelItem: Hashable { }

SectionIdentifier๋กœ ์‚ฌ์šฉ๋  SectionModelType๊ณผ ItemIdentifier๋กœ ์‚ฌ์šฉ๋  SectionModelItem ํ”„๋กœํ† ์ฝœ์ž…๋‹ˆ๋‹ค.

์ด ๋‘๊ฐ€์ง€ ํ”„๋กœํ† ์ฝœ์€ ์•„๋ฌด ๊ธฐ๋Šฅ๋„ ํ•˜์ง€ ์•Š๊ณ  ์˜ค์ง Hashable์„ ์ฑ„ํƒํ•ด์ฃผ๊ธฐ๋งŒ ํ–ˆ์Šต๋‹ˆ๋‹ค. (DiffableDataSources์—์„œ ์‚ฌ์šฉ๋˜๋ ค๋ฉด Hashableํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค!)

์ถ”ํ›„์— ํ•„์š”ํ•˜๋‹ค๋ฉด ๊ธฐ๋Šฅ์„ ์ถ”๊ฐ€ํ•ด์ฃผ๊ฒ ์ง€๋งŒ ์•„์ง์€ Generic์—์„œ์˜ ํƒ€์ž… ์ œํ•œ ์šฉ๋„๋กœ๋งŒ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

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
}

๊ทธ๋ฆฌ๊ณ  ๊ทธ ๋‘๊ฐ€์ง€ ํ”„๋กœํ† ์ฝœ์„ ์ฑ„ํƒํ•œ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.

SectionModelType์„ ์ฑ„ํƒํ•œ Section๋“ค๊ณผ SectionModelItem์„ ์ฑ„ํƒํ•œ Item์„ ์ •์˜ํ•ด์ค๋‹ˆ๋‹ค.

2. Reactor State ๋ณ€๊ฒฝ

var sections: [ProfileSection] = []
var items: [[ProfileSectionItem]] = []

var profileSetupHelperItems: [ProfileSectionItem] = []
var preferencesItems: [ProfileSectionItem] = []
var anniversaryItems: [ProfileSectionItem] = []
var friendItems: [ProfileSectionItem] = []

์ €๋Š” ๊ฐ Section์— ๋Œ€์‘๋˜๋Š” Item๋“ค์„ ๋”ฐ๋กœ ๊ด€๋ฆฌํ•ด์ฃผ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋ ‡๊ฒŒ ํ•˜๋Š” ํŽธ์ด ๋ฐ์ดํ„ฐ๋“ค์„ ์ˆ˜์ •ํ•ด์ฃผ๋Š” ๋กœ์ง์„ ์งค ๋–„ ํŽธํ•˜๋”๋ผ๊ตฌ์š”..

func transform(state: Observable<State>) -> Observable<State> {
  return state.map { state in
    var newState = state
    var newSections: [ProfileSection] = []
    var newItems: [[ProfileSectionItem]] = []
    var profileSetupHelperItems: [ProfileSectionItem] = []

    // ์ทจํ–ฅ
    if !state.preferencesItems.isEmpty {
	  newSections.append(.preferences)
	  newItems.append(state.preferencesItems)
    } else {
    profileSetupHelperItems.append(.profileSetupHelper(ProfileSetupHelperCellReactor(.preference)))
    }
    // ๊ธฐ๋…์ผ
    if !state.anniversaryItems.isEmpty {
	  newSections.append(.anniversaries)
	  newItems.append(state.anniversaryItems)
    } else {
    profileSetupHelperItems.append(.profileSetupHelper(ProfileSetupHelperCellReactor(.anniversary)))
    }
    // ์นœ๊ตฌ
    newSections.append(.friends)
    newItems.append(state.friendItems)
    // ์ƒˆ ํ”„๋กœํ•„
    if !profileSetupHelperItems.isEmpty {
	  newSections.insert(.profileSetupHelper, at: .zero)
	  newItems.insert(profileSetupHelperItems, at: .zero)
    }
  
    newState.sections = newSections
    newState.items = newItems
    return newState
  }
}

๊ฐ Item๋“ค์— ๋Œ€ํ•œ ๋ฐ์ดํ„ฐ๋“ค์€ transform(state:)์—์„œ ํ†ตํ•ฉํ•ด์ฃผ๋Š” ๋ฐฉ์‹์œผ๋กœ ์ฒ˜๋ฆฌํ•ด์ฃผ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

3. DataSource์— ๋ฐ”์ธ๋”ฉ

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)

sections์™€ items ๋ชจ๋‘ Hashableํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์œ„์™€ ๊ฐ™์ด appendSections, appendItems ๋“ฑ์˜ DiffableDataSources์˜ API๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ dataSource์— ๋ฐ”์ธ๋”ฉํ•ด์ฃผ๋ฉด ๋์ž…๋‹ˆ๋‹ค!