본문 바로가기

Project/MovieClip

🤔 영화 정보를 통합해서 관리할 필요가 있나?

 

✅ 현재TMDB에서 API를 통해 받아오는 데이터를 관리하는 구조 

  • TvTMDBData & TvTMDBResult 구조체: API를 통해 받아오는 데이터를 직접 관리 
  • TvCombineData 구조체: TvTMDBData를 배열로 통합해서 관리하는 구조체 
  • TvResultItem 구조체: TvTMDBData타입의 데이터와 별도의 섹션을 구분하는 데이터를 관리하는 구조체

 

영화를 받아오는 정보 외에 다른 부분을 컬렉션뷰에 동일하게 넣되, 레이아웃은 유지하고 싶다면?

굳이 데이터 모델을 하나로 통합할 필요가 있나? 즉 섹션마다 다른 데이터 모델을 사용하려면?


🌟 해결 방법

각 섹션마다 다른 데이터 모델을 사용하려면 다양한 데이터 모델을 포함할 수 있는 enum 타입을 만들어서 활용하면 됩니다.

 

✅ 데이터 모델 설계

enum SectionType {
    case fruit
    case family
}

struct Fruit {
    let name: String
}

struct Family {
    let dad: String
}

 

각 섹션에 맞는 아이템을 저장할 수 있도록 열거형을 활용한 Item 모델을 만듭니다.

enum Item {
    case fruit(Fruit)
    case family(Family)
}

 

 

✅ Diffable Data Source 적용

Diffable Data Source에서 섹션과 아이템을 다르게 관리할 수 있도록 설정합니다.

typealias DataSource = UICollectionViewDiffableDataSource<SectionType, Item>

 

이제 Item이 UICollectionViewDiffableDataSource의 아이템 역할을 하므로, 섹션마다 다른 타입을 넣을 수 있습니다.

 

✅ 데이터 설정 (스냅샷)

이제 스냅샷을 만들어 섹션별로 다른 데이터를 추가하면 됩니다.

var snapshot = NSDiffableDataSourceSnapshot<SectionType, Item>()

let fruits = [Fruit(name: "Apple"), Fruit(name: "Banana")].map { Item.fruit($0) }
let family = [Family(dad: "John")].map { Item.family($0) }

snapshot.appendSections([.fruit, .family])
snapshot.appendItems(fruits, toSection: .fruit)
snapshot.appendItems(family, toSection: .family)

dataSource.apply(snapshot, animatingDifferences: true)

 

 

✅ 셀 구성 (cellProvider)

셀을 설정할 때, switch 문을 사용하여 각 Item에 맞는 셀을 반환하면 됩니다.

dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, item in
    switch item {
    case .fruit(let fruit):
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FruitCell", for: indexPath) as! FruitCell
        cell.configure(with: fruit)
        return cell
    case .family(let family):
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "FamilyCell", for: indexPath) as! FamilyCell
        cell.configure(with: family)
        return cell
    }
}

 

 

✅ 결론

✔️ enum Item을 사용하면 여러 가지 데이터 모델을 하나의 컬렉션 뷰에서 관리 가능
✔️ UICollectionViewDiffableDataSource를 활용하여 섹션별로 다른 데이터를 적용
✔️ UICollectionViewCompositionalLayout을 활용하면 섹션별 레이아웃도 다르게 구성 가능


 

🌟 데이터 모델이 동일하다면, 통합할 필요 없이 그대로 사용할 수 있지 않나?

✅ 섹션별로 개별 데이터 관리하는 방식

1️⃣ 섹션 정의 (영화 타입)

enum MovieSection: CaseIterable {
    case popular      // 인기 영화
    case nowPlaying   // 상영 중인 영화
    case upcoming     // 개봉 예정작
    case topRated     // 순위 높은 영화
}

 

2️⃣ 아이템 모델

모든 API의 데이터 모델이 동일하므로 Movie 모델을 그대로 사용

struct Movie: Hashable {
    let id: Int
    let title: String
    let releaseDate: String
    let posterURL: String
}

 

🔷 Movie 모델이 Hashable을 따르도록 해야 UICollectionViewDiffableDataSource에서 사용 가능

 

✅ Diffable Data Source 적용 

typealias DataSource = UICollectionViewDiffableDataSource<MovieSection, Movie>
var dataSource: DataSource!

 

 

✅ 데이터 로드 및 스냅샷 업데이트

각 API에서 받아온 데이터를 각 섹션에 맞게 적용

var snapshot = NSDiffableDataSourceSnapshot<MovieSection, Movie>()

let popularMovies: [Movie] = fetchPopularMovies()
let nowPlayingMovies: [Movie] = fetchNowPlayingMovies()
let upcomingMovies: [Movie] = fetchUpcomingMovies()
let topRatedMovies: [Movie] = fetchTopRatedMovies()

snapshot.appendSections(MovieSection.allCases)
snapshot.appendItems(popularMovies, toSection: .popular)
snapshot.appendItems(nowPlayingMovies, toSection: .nowPlaying)
snapshot.appendItems(upcomingMovies, toSection: .upcoming)
snapshot.appendItems(topRatedMovies, toSection: .topRated)

dataSource.apply(snapshot, animatingDifferences: true)

 

✅ 셀 구성

dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, movie in
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MovieCell", for: indexPath) as! MovieCell
    cell.configure(with: movie)
    return cell
}

 

🎯 결론

하나의 모델(Movie)로 통합할 필요 없이 API 별로 관리 가능
각 API의 데이터를 개별 섹션으로 분리하여, 더 명확한 구조 유지
UICollectionViewDiffableDataSource + UICollectionViewCompositionalLayout을 활용하면 깔끔하게 적용 가능

 

 

✨ 리팩토링된 데이터 모델

TvCombineData 제거 → TvTMDBData 자체를 배열로 관리
TvTMDBData에서 type 제거 → TvResultItem에서 섹션 타입을 관리하도록 변경
가독성 향상
데이터 흐름이 더 직관적으로 변함

// MARK: - TV Section Type
enum TvSectionType: String, Codable, Hashable {
    case airingToday = "Air Today"
    case onTheAir = "On The Air"
    case popular = "Popular"
    case topRated = "Top Rated"
}

// MARK: - TV Data from TMDB
struct TvTMDBData: Codable, Hashable {
    let page: Int
    let results: [TvTMDBResult]
    let totalPages: Int
    let totalResults: Int

    enum CodingKeys: String, CodingKey {
        case page, results
        case totalPages = "total_pages"
        case totalResults = "total_results"
    }
}

// MARK: - TV Show Details
struct TvTMDBResult: Codable, Hashable {
    let id: Int
    let name: String
    let originalName: String
    let overview: String
    let popularity: Double
    let voteAverage: Double
    let voteCount: Int
    let firstAirDate: String
    let originalLanguage: String
    let originCountry: [String]
    let posterPath: String?
    let backdropPath: String?
    let genreIDs: [Int]
    
    var genreNames: [String]? // 클라이언트에서 변환하여 사용

    enum CodingKeys: String, CodingKey {
        case id, name, overview, popularity
        case originalName = "original_name"
        case voteAverage = "vote_average"
        case voteCount = "vote_count"
        case firstAirDate = "first_air_date"
        case originalLanguage = "original_language"
        case originCountry = "origin_country"
        case posterPath = "poster_path"
        case backdropPath = "backdrop_path"
        case genreIDs = "genre_ids"
    }
}

// MARK: - TV Result Item for Diffable Data Source
struct TvResultItem: Hashable {
    let tvResult: TvTMDBResult
    let sectionType: TvSectionType

    func hash(into hasher: inout Hasher) {
        hasher.combine(tvResult.id)
        hasher.combine(sectionType)
    }
    
    static func == (lhs: TvResultItem, rhs: TvResultItem) -> Bool {
        return lhs.tvResult.id == rhs.tvResult.id &&
               lhs.sectionType == rhs.sectionType
    }
}