๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

iOS

[ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…] DiffableDataSource ์ปฌ๋ ‰์…˜ ๋ทฐ์— ๋‹ค์–‘ํ•œ ๋ฐ์ดํ„ฐ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•˜๊ธฐ

๐Ÿ˜ ๋ฌธ์ œ ๋ฐฐ๊ฒฝ

ํ”„๋กœ์ ํŠธ ์ง„ํ–‰์ค‘์— CollecionView๋ฅผ ์ค‘๋ณต์œผ๋กœ ์‚ฌ์šฉํ•˜๋ฉด์„œ ๋ฐœ์ƒํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๊ฒ€์ƒ‰ ํŽ˜์ด์ง€ ์˜ˆ๋งค๋‚ด์—ญ ๋”๋ณด๊ธฐ ํŽ˜์ด์ง€ ์ฐœ ๋ชฉ๋ก ๋”๋ณด๊ธฐ ํŽ˜์ด์ง€

 

์œ„ 3๊ฐœ์˜ View๋Š” ๋ชจ๋‘ ๋‹ค๋ฅธ ViewController์ž…๋‹ˆ๋‹ค.

๊ทธ๋ฆฌ๊ณ  ์•ˆ์— ์žˆ๋Š” ์ปฌ๋ ‰์…˜ ๋ทฐ๋Š” ๊ฐ VC์— ์ƒ์„ฑ ๋ฐ ๊ตฌ์„ฑ์ž‘์—…์„ ํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ตฌํ˜„ํ•˜๊ณ  ๋ณด๋‹ˆ, ์ปฌ๋ ‰์…˜๋ทฐ์˜ ๋””์ž์ธ์ด ๋™์ผํ•œ๋ฐ ๊ฐ VC์—์„œ ์ƒ์„ฑํ•˜๋‹ค๋ณด๋‹ˆ ์ค‘๋ณต๋˜๋Š” ๋А๋‚Œ์„ ๋ฐ›์•˜๊ณ , VC์— ์ฝ”๋“œ์ค„์ด ๊ธธ์–ด์ง€๋ฉด์„œ ๊ฐ€๋…์„ฑ๋„ ๋–จ์–ด์ง€๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

-> ๊ทธ๋ž˜์„œ ์ด๋ฅผ ํ•˜๋‚˜์˜ ์ปฌ๋ ‰์…˜ ๋ทฐ๋กœ ํŒŒ์ผ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ตฌ์„ฑํ•œ ๋‹ค์Œ ์žฌ์‚ฌ์šฉํ•˜๋ฉด VC๊ฐ€ ๋” ๊ฐ„๊ฒฐํ•ด์ง€๊ฒ ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค!

 

๐Ÿค” ์ƒํ™ฉ

์žฌ์‚ฌ์šฉ ํ•˜๊ธฐ์ „์— ๊ฐ ์ปฌ๋ ‰์…˜ ๋ทฐ์˜ ๊ณตํ†ต์ ์ด ์žˆ๋Š”์ง€ ํŒŒ์•…์„ ๋จผ์ € ํ–ˆ์Šต๋‹ˆ๋‹ค.

1. 3๊ฐœ์˜ ์ปฌ๋ ‰์…˜ ๋ทฐ ๋ชจ๋‘ DiffableDataSource๋ฅผ ์‚ฌ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

2. 3๊ฐœ์˜ ์ปฌ๋ ‰์…˜ ๋ทฐ์˜ ์…€ ๋ ˆ์ด์•„์›ƒ ๊ตฌ์„ฑ์ด ๊ฐ™์Šต๋‹ˆ๋‹ค.

3. 3๊ฐœ์˜ ์ปฌ๋ ‰์…˜ ๋ทฐ ์…€์€ ๋ชจ๋‘ ์ด๋ฏธ์ง€๋ทฐ ํ•˜๋‚˜๋กœ ์ด๋ฃจ์–ด์ ธ์žˆ์Šต๋‹ˆ๋‹ค.

4. 3๊ฐœ์˜ ์ปฌ๋ ‰์…˜ ๋ทฐ ๋ชจ๋‘ APIํ˜ธ์ถœ์„ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์˜ต๋‹ˆ๋‹ค.

 

 

๊ทธ๋ฆฌ๊ณ  ๊ตฌํ˜„ํ•  ๋•Œ ๋ถ„๊ธฐ์ ์„ ๊ตฌ์„ฑํ•˜๊ธฐ ์œ„ํ•ด ์ฐจ์ด์ ๋„ ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

- ๊ฒ€์ƒ‰ํŽ˜์ด์ง€์™€ ์ฐœ ๋ชฉ๋ก ๋”๋ณด๊ธฐ ํŽ˜์ด์ง€์˜ ๋ฐ์ดํ„ฐ๋ชจ๋ธ์€ MovieEntity๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

- ์˜ˆ๋งค๋‚ด์—ญ ๋”๋ณด๊ธฐ ํŽ˜์ด์ง€๋Š” Reservation ๋ฐ์ดํ„ฐ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

- UseCase์™€ Repository๊ฐ€ ๊ฐ๊ฐ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

 

์ข…ํ•ฉํ•˜์ž๋ฉด 3๊ฐœ์˜ ์ปฌ๋ ‰์…˜ ๋ทฐ์˜ UI ๊ตฌ์„ฑ์€ ๋ชจ๋‘ ๊ฐ™์ง€๋งŒ, ์…€ ๊ตฌ์„ฑ์‹œ ๋ฐ์ดํ„ฐ๋ชจ๋ธ์—์„œ ์ฐจ์ด์ ๊ณผ UseCase, Repository๊ฐ€ ๋‹ค๋ฅด๋‹ค๋Š” ์ ์ž…๋‹ˆ๋‹ค.

 

๐Ÿ˜Ž ํ•ด๊ฒฐ ์•„์ด๋””์–ด 1: ๋ฐ์ดํ„ฐ๋ชจ๋ธ์„ ํ†ตํ•ฉํ•˜์ž

๋ฐ์ดํ„ฐ๋ชจ๋ธ๋งŒ ์ฐจ์ด๊ฐ€ ์žˆ์œผ๋ฏ€๋กœ ๋ฐ์ดํ„ฐ๋ชจ๋ธ์„ ํ†ตํ•ฉํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ๋จผ์ € ๋– ์˜ฌ๋ž์Šต๋‹ˆ๋‹ค.

// ์˜ํ™”๊ฒ€์ƒ‰ ํŽ˜์ด์ง€, ์ฐœ๋ชฉ๋ก ๋”๋ณด๊ธฐ ํŽ˜์ด์ง€์˜ ๋ฐ์ดํ„ฐ๋ชจ๋ธ
struct MovieEntity: Hashable {
    let movieID: Int                // ์˜ํ™” ID
    let title: String               // ์˜ํ™” ์ œ๋ชฉ
    let genre: [String]             // ์žฅ๋ฅด
    let director: String            // ์˜ํ™” ๊ฐ๋…
    let actors: [String]            // ์ถœ์—ฐ ๋ฐฐ์šฐ (3๋ช…)
    let releaseDate: String         // ์˜ํ™” ๊ฐœ๋ด‰์ผ
    let runtime: Int                // ์ƒ์˜์‹œ๊ฐ„
    let voteAverage: Double         // ํ‰์ (์†Œ์ˆ˜์  ๋‘์ž๋ฆฌ๋งŒ ์‚ฌ์šฉ)
    let voteCount: Int              // ํ‰๊ฐ€ ์ธ์› ์ˆ˜
    let overview: String            // ์ค„๊ฑฐ๋ฆฌ
    let posterImageURL: String      // ํฌ์Šคํ„ฐ ์ด๋ฏธ์ง€ URL
    let certification: String       // ๊ด€๋žŒ ๋“ฑ๊ธ‰
}
// ์˜ˆ๋งค๋‚ด์—ญ ๋”๋ณด๊ธฐ ๋ฐ์ดํ„ฐ๋ชจ๋ธ
struct Reservation: Hashable {
    let reservationID: String
    let genre: [String]
    let member: Int
    let posterURL: String
    let reservationTime: String
    let title: String
}

 

๋‘ ๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ชจ๋ธ์„ ํ†ตํ•ฉํ•˜๊ณ  DiffableDataSource๋ฅผ ์ •์˜ํ•˜๋ฉด ๋˜๊ฒ ๋‹ค๋ผ๊ณ  ์ƒ๊ฐ์ด ๋“ค์—ˆ์ง€๋งŒ...

ํ”„๋กœ์ ํŠธ์˜ ๊ฐ€์žฅ ์•ˆ์ชฝ์˜ ๋ ˆ์ด์–ด์ธ ๋ชจ๋ธ์„ ๋ณ€๊ฒฝํ•˜๋ฉด UseCase์™€ Respository์˜ ๋ฉ”์„œ๋“œ๋“ค์„ ๋ชจ๋‘ ๋ณ€๊ฒฝํ•ด์•ผ ํ•˜๋Š” ๋‹จ์ ์„ ์•Œ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

 

๋˜ํ•œ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ๋ฐ์ดํ„ฐ๋„ ์ €์žฅ๋œ๋‹ค๋Š” ๋‹จ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด ์˜ˆ๋งค๋‚ด์—ญ์—์„œ๋Š” overview(์ค„๊ฑฐ๋ฆฌ), certification(๊ด€๋žŒ๋“ฑ๊ธ‰) ํ•ญ๋ชฉ๋“ค์€ ํ•„์š”๊ฐ€ ์—†์–ด์„œ ๋ฐ์ดํ„ฐ๊ณต๊ฐ„์„ ์ฐจ์ง€ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

์˜ˆ๋งค๋‚ด์—ญ ๋ฐ์ดํ„ฐ๊ฐ€ ํŒŒ์ด์–ด๋ฒ ์ด์Šค์— ์ €์žฅ๋œ ๋ชจ์Šต ์ฐœ ๋ชฉ๋ก ๋ฐ์ดํ„ฐ๊ฐ€ ํŒŒ์ด์–ด๋ฒ ์ด์Šค์— ์ €์žฅ๋œ ๋ชจ์Šต

ํ•ด๋‹น ๋ฐฉ๋ฒ•์€ ํ‹€๋ฆฌ๊ณ  ์ž‘๋™์ด ์•ˆ๋˜๋Š” ์•„์ด๋””์–ด๋Š” ์•„๋‹ˆ์ง€๋งŒ,

๋งŽ์€ ์ฝ”๋“œ๋ฅผ ๋ณ€๊ฒฝํ•ด์•ผํ•œ๋‹ค๋Š” ์ ๊ณผ ๋ฐ์ดํ„ฐ ๊ณต๊ฐ„์„ ์ ˆ์•ฝํ•˜๊ธฐ ์œ„ํ•˜์—ฌ ํ•ด๋‹น ์•„์ด๋””์–ด๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

 

 

๐Ÿ˜Ž ํ•ด๊ฒฐ ์•„์ด๋””์–ด 2: ๋‹ค์–‘ํ•œ ๋ฐ์ดํ„ฐ๋ชจ๋ธ์„ ๋ฐ›๋„๋ก DiffableDataSource๋ฅผ ์ •์˜ํ•˜์ž

์ด ๊ธ€์˜ ํ•ต์‹ฌ๋‚ด์šฉ์ด ์—ฌ๊ธฐ์„œ ๋‚˜์˜ค๋Š”๋ฐ์š”. 

DiffableDataSource๋ฅผ ์ •์˜ํ•  ๋•Œ ๋‹ค์–‘ํ•œ ๋ฐ์ดํ„ฐ๋ชจ๋ธ์„ ๋ฐ›๋„๋กํ•˜์—ฌ ์ปฌ๋ ‰์…˜ ๋ทฐ์˜ ์žฌ์‚ฌ์šฉ์„ฑ์„ ๋Š˜๋ฆฌ๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

์ด๋Š” ๊ฐ VC์˜ UseCase์™€ Repository๋ฅผ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•˜๋ฉด์„œ ๋ฐ์ดํ„ฐ๋ชจ๋ธ๋งŒ ์œ ์—ฐํ•˜๊ฒŒ ๋ฐ›๋„๋ก ํ•˜์—ฌ 1๋ฒˆ์˜ ์•„์ด๋””์–ด๋ณด๋‹ค ์ ๊ฒŒ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๊ณ , ์ด๋ฏธ ๊ตฌํ˜„๋œ ๋ฐ์ดํ„ฐ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋งŒ ์ €์žฅํ•˜๊ณ  ์‚ฌ์šฉํ•˜๋„๋ก ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.

 

// ๋ฐ์ดํ„ฐ๋ชจ๋ธ์„ ์—ด๊ฑฐํ˜•์œผ๋กœ ๊ตฌํ˜„
enum ItemWrapper: Hashable {
    case movieEntity(MovieEntity)
    case reservationEntity(Reservation)
}

typealias DataSource = UICollectionViewDiffableDataSource<Section, ItemWrapper>
typealias SnapShot = NSDiffableDataSourceSnapshot<Section, ItemWrapper>

// ์…€ ๋“ฑ๋ก ์ •์˜
let cellRegistration = UICollectionView.CellRegistration<MovieItemCollectionViewCell, [ItemWrapper]> { cell,indexPath,itemIdentifier in }

// ์…€ ๊ตฌ์„ฑํ•  ๋•Œ ๋ถ„๊ธฐ์ ์„ ์ƒ์„ฑํ•˜์—ฌ ํ•ด๋‹น ์…€์„ configureํ•˜๋„๋ก ์ˆ˜์ •
movieItemCollectionDataSource = DataSource(collectionView: self) { collectionView, indexPath, item in
    let cell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: [item])
    switch item {
    case .movieEntity(let movieEntity):
        cell.configure(model: movieEntity)
    case .reservationEntity(let reservationEntity):
        cell.configure(model: reservationEntity)
    }
    return cell
}

// Item ์Šค๋ƒ…์ƒท ์ƒ์„ฑ
private func makeSnapShot(item: [CellConfigurable]) -> SnapShot {
    var snapshot = SnapShot()
    snapshot.appendSections([.main])
    if item is [MovieEntity] {
        snapshot.appendItems(item.map{ItemWrapper.movieEntity($0 as! MovieEntity)})
    }
    if item is [Reservation] {
        snapshot.appendItems(item.map{ItemWrapper.reservationEntity($0 as! Reservation)})
    }
    
    return snapshot
}

 

๋˜ํ•œ ์ปค์Šคํ…€ ์…€์˜ configure๋ฉ”์„œ๋“œ์—์„œ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ๊ฐ ๋ฐ์ดํ„ฐ๋ชจ๋ธ์„ ๋ฐ›๋˜๊ฑธ ์—ฌ๋Ÿฌ๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋ชจ๋ธ์„ ๋ฐ›๋„๋ก ํ•˜๊ธฐ ์œ„ํ•ด ํ”„๋กœํ† ์ฝœ์„ ์ƒ์„ฑํ•˜๊ณ  ๋ฐ์ดํ„ฐ๋ชจ๋ธ๋“ค์ด ์ฑ„ํƒํ•˜๊ฒŒ ์ˆ˜์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

๋งค๊ฐœ๋ณ€์ˆ˜์—๋Š” ํ•ด๋‹น ํ”„๋กœํ† ์ฝœ์„ ์ฑ„ํƒํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ชจ๋ธ์ด ๋“ค์–ด์˜ค๋„๋ก ์ˆ˜์ •ํ•˜์˜€์Šต๋‹ˆ๋‹ค.

// ๋ฐ์ดํ„ฐ๋ชจ๋ธ์˜ ๊ณตํ†ต ํƒ€์ž…์„ ๋งค๊ฐœ๋ณ€์ˆ˜๋กœ ์ „๋‹ฌํ•˜๊ธฐ ์œ„ํ•ด ํ”„๋กœํ† ์ฝœ ์ƒ์„ฑ
protocol CellConfigurable { }
// ํ”„๋กœํ† ์ฝœ ์ฑ„ํƒ
extension MovieEntity: CellConfigurable { }
extension Reservation: CellConfigurable { }

// CustomCell์˜ configure ๋ฉ”์„œ๋“œ
func configure(model: CellConfigurable) {
    if let model = model as? MovieEntity {
        // ์˜ˆ๋งค๋‚ด์—ญ UI ๊ตฌ์„ฑ
    }
    if let model = model as? Reservation {
        // ์ฐœ๋ชฉ๋ก UI ๊ตฌ์„ฑ
    }
}