iOS/UIKIT

API를 통해 가져온 데이터를 테이블 내에 컬렉션뷰 이미지로 넣기

밤새는 탐험가89 2024. 7. 5. 02:45

https://explorer89.tistory.com/103

 

공공 API를 통해 데이터 불러오기 (TMDB)

구현 내용TMDB 라는 영화 사이트 내에서 제공하는 API를 통해 외부 데이터를 갖고 왔다. 구현 코드APICaller.swift 라는 클래스 생성한다.import Foundationstruct Constants { static let API_KEY = "개인 API_KEY" static l

explorer89.tistory.com

 

 

 

구현 내용

  • 테이블 내 컬렉셔뷰에 API를 통해 얻은 데이터 중에서 이미지를 갖고와 보여준다. 

 

구현 코드

  • TMDB 사이트 내에 API를 받는 개발자 사이트로 들어간다. 
  • 각 카테고리 별로 제공하는 API 함수를 받아온다. 

https://developer.themoviedb.org/reference/movie-top-rated-list

 

Top Rated

Get a list of movies ordered by rating.

developer.themoviedb.org

 

 

  • 사이트에서 제공하는 함수를 참고한다. 

 

 

  • 아래 결과는 사이트에서 제공하는 샘플 함수를 구현했을 때 나오는 것으로. 이를 토대로 데이터 모델을 설계한다. 
{
  "page": 1,
  "results": [
    {
      "adult": false,
      "backdrop_path": "/zfbjgQE1uSd9wiPTX4VzsLi0rGG.jpg",
      "genre_ids": [
        18,
        80
      ],
      "id": 278,
      "original_language": "en",
      "original_title": "The Shawshank Redemption",
      "overview": "Imprisoned in the 1940s for the double murder of his wife and her lover, upstanding banker Andy Dufresne begins a new life at the Shawshank prison, where he puts his accounting skills to work for an amoral warden. During his long stretch in prison, Dufresne comes to be admired by the other inmates -- including an older prisoner named Red -- for his integrity and unquenchable sense of hope.",
      "popularity": 120.189,
      "poster_path": "/9cqNxx0GxF0bflZmeSMuL5tnGzr.jpg",
      "release_date": "1994-09-23",
      "title": "The Shawshank Redemption",
      "video": false,
      "vote_average": 8.705,
      "vote_count": 26396
    },
    ...
}

 

 

  • 프로젝트 내에 [Models] 라는 폴더를 하나 생성하고, 여기에 Title.swift 라는 이름으로 데이터 모델을 생성한다. 
import Foundation

struct TrendingTitleResponse: Codable {
    let results: [Title]
}

struct Title: Codable {
    let id: Int
    let media_type: String?
    let original_name: String?
    let original_title: String?
    let poster_path: String?
    let overviews: String?
    let vote_count: Int
    let release_date: String?
    let vote_average: Double
}

 

 

  • [Managers] 라는 폴더를 생성하고, APICaller 라는 이름으로 파일을 생성하고, API를 통해 데이터를 불러올 함수를 설계한다.
import Foundation

// MARK: Constants
struct Constants {
    static let baseURL = "https://api.themoviedb.org"
    static let API_KEY = 사용자에게 할당된 API_KEY를 넣는다. 
}

// MARK: ERROR
enum APIError: Error {
    case failedToGetData
}


// MARK: APICaller 클래스
class APICaller {
    
    static let shared = APICaller()
    
    
    func getTrendingMovies(completion: @escaping (Result<[Title], Error>) -> Void) {
        
        let url = URL(string: "https://api.themoviedb.org/3/trending/movie/day")!
        var components = URLComponents(url: url, resolvingAgainstBaseURL: true)!
        let queryItems: [URLQueryItem] = [
            URLQueryItem(name: "language", value: "en-US"),
        ]
        components.queryItems = components.queryItems.map { $0 + queryItems } ?? queryItems
        
        var request = URLRequest(url: components.url!)
        request.httpMethod = "GET"
        request.timeoutInterval = 10
        request.allHTTPHeaderFields = [
            "accept": "application/json",
            "Authorization": "Bearer \(Constants.API_KEY)"
        ]
        
        let task = URLSession.shared.dataTask(with: request) { data, response, error in
            if let error = error {
                print("Error occurred: \(error.localizedDescription)")
                completion(.failure(APIError.failedToGetData))
                return
            }
            guard let data = data else {
                print("No data received")
                completion(.failure(APIError.failedToGetData))
                return
            }
            do {
                // Print JSON data to debug
//                if let json = try? JSONSerialization.jsonObject(with: data, options: []) {
//                    print("JSON Response: \(json)")
//                }
                let results = try JSONDecoder().decode(TrendingTitleResponse.self, from: data)
                completion(.success(results.results))
            } catch {
                print("JSON Decoding error: \(error.localizedDescription)")
                completion(.failure(APIError.failedToGetData))
            }
        }
        task.resume()
    }
    
    
    func getTrendingTvs(completion: @escaping (Result<[Title], Error>) -> Void) {
        let url = URL(string: "https://api.themoviedb.org/3/trending/tv/day")!
        var components = URLComponents(url: url, resolvingAgainstBaseURL: true)!
        let queryItems: [URLQueryItem] = [
            URLQueryItem(name: "language", value: "en-US"),
        ]
        components.queryItems = components.queryItems.map { $0 + queryItems } ?? queryItems
        
        var request = URLRequest(url: components.url!)
        request.httpMethod = "GET"
        request.timeoutInterval = 10
        request.allHTTPHeaderFields = [
            "accept": "application/json",
            "Authorization": "Bearer \(Constants.API_KEY)"
        ]
        
        let task = URLSession.shared.dataTask(with: request) { data, response, error in
            if let error = error {
                print("Error occurred: \(error.localizedDescription)")
                completion(.failure(APIError.failedToGetData))
                return
            }
            guard let data = data else {
                print("No data received")
                completion(.failure(APIError.failedToGetData))
                return
            }
            do {
                // Print JSON data to debug
//                if let json = try? JSONSerialization.jsonObject(with: data, options: []) {
//                    print("JSON Response: \(json)")
//                }
                let results = try JSONDecoder().decode(TrendingTitleResponse.self, from: data)
                completion(.success(results.results))
            } catch {
                print("JSON Decoding error: \(error.localizedDescription)")
                completion(.failure(APIError.failedToGetData))
            }
        }
        task.resume()
    }
    
    func getPopular(completion: @escaping (Result<[Title], Error>) -> Void) {
        let url = URL(string: "https://api.themoviedb.org/3/movie/popular")!
        
        var components = URLComponents(url: url, resolvingAgainstBaseURL: true)!
        let queryItems: [URLQueryItem] = [
            URLQueryItem(name: "language", value: "en-US"),
            URLQueryItem(name: "page", value: "1"),
        ]
        components.queryItems = components.queryItems.map { $0 + queryItems } ?? queryItems
        
        var request = URLRequest(url: components.url!)
        request.httpMethod = "GET"
        request.timeoutInterval = 10
        request.allHTTPHeaderFields = [
            "accept": "application/json",
            "Authorization": "Bearer \(Constants.API_KEY)"
        ]
        
        let task = URLSession.shared.dataTask(with: request) { data, response, error in
            if let error = error {
                print("Error occurred: \(error.localizedDescription)")
                completion(.failure(APIError.failedToGetData))
                return
            }
            guard let data = data else {
                print("No data received")
                completion(.failure(APIError.failedToGetData))
                return
            }
            do {
                // Print JSON data to debug
//                if let json = try? JSONSerialization.jsonObject(with: data, options: []) {
//                    print("JSON Response: \(json)")
//                }
                let results = try JSONDecoder().decode(TrendingTitleResponse.self, from: data)
                completion(.success(results.results))
            } catch {
                print("JSON Decoding error: \(error.localizedDescription)")
                completion(.failure(APIError.failedToGetData))
            }
        }
        task.resume()
    }
    
    func getUpcomingMovies(completion: @escaping (Result<[Title], Error>) -> Void) {
        let url = URL(string: "https://api.themoviedb.org/3/movie/upcoming")!
        var components = URLComponents(url: url, resolvingAgainstBaseURL: true)!
        let queryItems: [URLQueryItem] = [
            URLQueryItem(name: "language", value: "en-US"),
            URLQueryItem(name: "page", value: "1"),
        ]
        components.queryItems = components.queryItems.map { $0 + queryItems } ?? queryItems
        
        var request = URLRequest(url: components.url!)
        request.httpMethod = "GET"
        request.timeoutInterval = 10
        request.allHTTPHeaderFields = [
            "accept": "application/json",
            "Authorization": "Bearer \(Constants.API_KEY)"
        ]
        
        let task = URLSession.shared.dataTask(with: request) { data, response, error in
            if let error = error {
                print("Error occurred: \(error.localizedDescription)")
                completion(.failure(APIError.failedToGetData))
                return
            }
            guard let data = data else {
                print("No data received")
                completion(.failure(APIError.failedToGetData))
                return
            }
            do {
                // Print JSON data to debug
//                if let json = try? JSONSerialization.jsonObject(with: data, options: []) {
//                    print("JSON Response: \(json)")
//                }
                let results = try JSONDecoder().decode(TrendingTitleResponse.self, from: data)
                completion(.success(results.results))
            } catch {
                print("JSON Decoding error: \(error.localizedDescription)")
                completion(.failure(APIError.failedToGetData))
            }
        }
        task.resume()
    }
    
    func getTopRated(completion: @escaping (Result<[Title], Error>) -> Void) {
        let url = URL(string: "https://api.themoviedb.org/3/movie/top_rated")!
        var components = URLComponents(url: url, resolvingAgainstBaseURL: true)!
        let queryItems: [URLQueryItem] = [
            URLQueryItem(name: "language", value: "en-US"),
            URLQueryItem(name: "page", value: "1"),
        ]
        components.queryItems = components.queryItems.map { $0 + queryItems } ?? queryItems
        
        var request = URLRequest(url: components.url!)
        request.httpMethod = "GET"
        request.timeoutInterval = 10
        request.allHTTPHeaderFields = [
            "accept": "application/json",
            "Authorization": "Bearer \(Constants.API_KEY)"
        ]
        
        let task = URLSession.shared.dataTask(with: request) { data, response, error in
            if let error = error {
                print("Error occurred: \(error.localizedDescription)")
                completion(.failure(APIError.failedToGetData))
                return
            }
            guard let data = data else {
                print("No data received")
                completion(.failure(APIError.failedToGetData))
                return
            }
            do {
                // Print JSON data to debug
//                if let json = try? JSONSerialization.jsonObject(with: data, options: []) {
//                    print("JSON Response: \(json)")
//                }
                let results = try JSONDecoder().decode(TrendingTitleResponse.self, from: data)
                completion(.success(results.results))
            } catch {
                print("JSON Decoding error: \(error.localizedDescription)")
                completion(.failure(APIError.failedToGetData))
            }
        }
        task.resume()
    }
}

 

 

  • 아래 코드를 대표로 자세히 알아본다.
  • 여기서  "completion: @escaping (Result<[Title], Error>) -> Void" 는 비동기 작업의 결과를 처리하기 위해 사용되는 클로저로, 이 클로저를 통해 함수가 완료되었을 때 호출자가 결과를 받을 수 있게 됩니다.

비동기 작업과 클로저

Swift에서 클로저는 기본적으로 비동기 작업이 끝나기 전에 함수가 반환되면 클로저도 함께 사라진다. 하지만 네트워크 요청과 같은 비동기 작업은 함수가 반환된 후에 완료될 수 있으므로, 클로저를 함수의 스코프 밖에서도 유지해야 한다. 이를 위해 @escaping 키워드를 사용한다. 

func someFunctionWithEscapingClosure(completion: @escaping () -> Void) {
    // 비동기 작업
    DispatchQueue.global().async {
        // 작업이 완료된 후 클로저 호출
        completion()
    }
}
func getTrendingMovies(completion: @escaping (Result<[Title], Error>) -> Void) {

    let url = URL(string: "https://api.themoviedb.org/3/trending/movie/day")!
    var components = URLComponents(url: url, resolvingAgainstBaseURL: true)!
    let queryItems: [URLQueryItem] = [
        URLQueryItem(name: "language", value: "en-US"),
    ]
    components.queryItems = components.queryItems.map { $0 + queryItems } ?? queryItems

    var request = URLRequest(url: components.url!)
    request.httpMethod = "GET"
    request.timeoutInterval = 10
    request.allHTTPHeaderFields = [
        "accept": "application/json",
        "Authorization": "Bearer \(Constants.API_KEY)"
    ]

    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        if let error = error {
            print("Error occurred: \(error.localizedDescription)")
            completion(.failure(APIError.failedToGetData))
            return
        }
        guard let data = data else {
            print("No data received")
            completion(.failure(APIError.failedToGetData))
            return
        }
        do {
            // Print JSON data to debug
//                if let json = try? JSONSerialization.jsonObject(with: data, options: []) {
//                    print("JSON Response: \(json)")
//                }
            let results = try JSONDecoder().decode(TrendingTitleResponse.self, from: data)
            completion(.success(results.results))
        } catch {
            print("JSON Decoding error: \(error.localizedDescription)")
            completion(.failure(APIError.failedToGetData))
        }
    }
    task.resume()
}

 

 

1. URL 설정

먼저, 트렌딩 영화 데이터를 가져오기 위한 기본 URL을 설정한다.

let url = URL(string: "https://api.themoviedb.org/3/trending/movie/day")!

 

 

2. URLComponents 설정

URL에 쿼리 파라미터를 추가하기 위해 URLComponents를 사용한다.

여기서 queryItems에는 API 요청 시 필요한 파라미터를 추가하는데, 여기서는 언어 설정만 포함하고 있습니다.

var components = URLComponents(url: url, resolvingAgainstBaseURL: true)!
let queryItems: [URLQueryItem] = [
    URLQueryItem(name: "language", value: "en-US"),
]
components.queryItems = components.queryItems.map { $0 + queryItems } ?? queryItems

 

 

3. URLRequest 설정

요청을 만들고 필요한 HTTP 메서드와 헤더를 설정한다.

 

  • httpMethod는 GET으로 설정한다.
  • timeoutInterval은 요청의 타임아웃 시간을 10초로 설정한다.
  • allHTTPHeaderFields에는 필요한 헤더를 추가하는데. 여기서는 accept와 Authorization 헤더를 추가한다.

 

var request = URLRequest(url: components.url!)
request.httpMethod = "GET"
request.timeoutInterval = 10
request.allHTTPHeaderFields = [
    "accept": "application/json",
    "Authorization": "Bearer \(Constants.API_KEY)"
]

 

4. URLSession 데이터 작업 생성 및 시작

URLSession을 사용하여 API 요청을 보내고, 응답을 처리한다.

 

  • dataTask(with:request)를 사용하여 요청을 보낸다.
  • 요청의 응답을 클로저를 통해 처리한다.
    • error가 발생하면 이를 출력하고, completion 핸들러를 통해 실패를 알린다.
    • 응답 데이터가 없으면 실패를 알린다.
    • JSON 데이터를 디코딩하여 TrendingTitleResponse 구조체로 변환하고, 성공을 알린다.
    • 디코딩 중 오류가 발생하면 실패를 알린다.

 

let task = URLSession.shared.dataTask(with: request) { data, response, error in
    if let error = error {
        print("Error occurred: \(error.localizedDescription)")
        completion(.failure(APIError.failedToGetData))
        return
    }
    guard let data = data else {
        print("No data received")
        completion(.failure(APIError.failedToGetData))
        return
    }
    do {
        // Print JSON data to debug
        // if let json = try? JSONSerialization.jsonObject(with: data, options: []) {
        //     print("JSON Response: \(json)")
        // }
        let results = try JSONDecoder().decode(TrendingTitleResponse.self, from: data)
        completion(.success(results.results))
    } catch {
        print("JSON Decoding error: \(error.localizedDescription)")
        completion(.failure(APIError.failedToGetData))
    }
}
task.resume()

 

 

⭐️ task.resume()는 URLSession 데이터 작업을 시작하기 위해 호출된다.

URLSession을 사용하여 네트워크 요청을 보낼 때, dataTask(with:completionHandler:) 메서드는 요청을 구성하고 URLSessionDataTask 객체를 반환하지만, 이 객체는 기본적으로 일시 중지된 상태로 생성된다.

네트워크 요청을 실제로 시작하려면 resume() 메서드를 호출해야 한다.

 

 

 

오픈 소스 중에 SDWebImage 를 통해 이미지를 처리한다. 

https://github.com/SDWebImage/SDWebImage

 

GitHub - SDWebImage/SDWebImage: Asynchronous image downloader with cache support as a UIImageView category

Asynchronous image downloader with cache support as a UIImageView category - SDWebImage/SDWebImage

github.com

 

위에 사이트에 들어가서 프로젝트에 AddPackage를 통해 삽입한다.

 

 

TitleCollectionViewCell.swift 파일을 생성한다. 

테이블 내에 컬렉션뷰를 삽입했다. 따라서 해당 컬렉션 뷰에 구현할 컬렉션 뷰 셀을 이 파일 안에 구현한다. 

import UIKit
import SDWebImage

class TitleCollectionViewCell: UICollectionViewCell {
    
    // MARK: Variables
    static let identifier = "TitleCollectionViewCell"
    
    // MARK: UI Components
    private let posterImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFill
        return imageView
    }()
    
    
    // MARK: Life Cycle
    override init(frame: CGRect) {
        super.init(frame: frame)
        contentView.addSubview(posterImageView)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        posterImageView.frame = contentView.bounds
    }
    
    // MARK: Functions
    func configure(with model: String) {
        guard let url = URL(string: "https://image.tmdb.org/t/p/w500/\(model)") else { return }
        
        posterImageView.sd_setImage(with: url, completed: nil)
    }
}

 

이 안에서 configure 함수는 url을 통해 이미지 경로를 받아오고, 이를 sd_setImage 메서드를 통해 posterImageView에 넣는다. 

 

 

CollectionViewTableViewCell.swift 파일 안에 위에 생성한 TitleCollectionViewCell을 컬렉션뷰에 등록한다. 

import UIKit

class CollectionViewTableViewCell: UITableViewCell {
    
	...
    // HomeViewController에서 받은 데이터를 담기 위한 변수 배열 
    private var titles: [Title] = []
    
    // MARK: UI Components
    private let collectionView: UICollectionView = {
        ...
        let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.register(TitleCollectionViewCell.self, forCellWithReuseIdentifier: TitleCollectionViewCell.identifier)
        // 스크롤 안내선 안보이게 함
        collectionView.showsHorizontalScrollIndicator = false
        return collectionView
    }()
	...

    func configure(with titles: [Title]) {
        self.titles = titles
        DispatchQueue.main.async { [weak self] in
            self?.collectionView.reloadData()
        }
    }
}

extension CollectionViewTableViewCell: UICollectionViewDelegate, UICollectionViewDataSource {
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: TitleCollectionViewCell.identifier, for: indexPath) as? TitleCollectionViewCell else { return UICollectionViewCell() }
        
        guard let model = titles[indexPath.row].poster_path else { return UICollectionViewCell() }
        print(model)
        cell.configure(with: model)
        return cell
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return titles.count
    }
}

 

CollectionViewTableViewCell은 HomeViewController 내에 있는 homeFeedTable에 등록된 것이다.

따라서 해당 테이블에 동작이 들어가면 이에 영향을 받는게 CollectionViewTableViewCell 이다. 

 

HomeViewController 내에서 외부 API를 통해 얻은 데이터를 받아오기 위해 configure 함수와 이를 담은 titles 변수 배열을 생성한다. 

실행하면 아래와 같이 이미지 경로 주소를 받아올 수 있다. 

 

 

 

⭐️ 왜? collectionView.reloadData()를 했나? HomeViewController내에 homeFeedTable을 reloadData() 하지 않고?

 

  • 세분화된 업데이트:
    • CollectionViewTableViewCell은 테이블 뷰의 한 셀에 해당한다. 이 셀 내부에 있는 컬렉션 뷰만 업데이트하면 되므로, 전체 테이블 뷰를 리로드할 필요가 없다.
    • 이는 성능 면에서 더 효율적인데, 전체 테이블 뷰를 리로드하는 것보다 특정 셀 내의 컬렉션 뷰만 리로드하는 것이 훨씬 빠르고 리소스를 적게 사용한다.
  • 책임 분리:
    • 각 UI 컴포넌트는 자신의 데이터를 관리하고 업데이트하는 것이 좋다. CollectionViewTableViewCell은 자신의 내부 상태(titles 배열)를 가지고 있으며, 이 상태가 변경될 때 자신의 UI(collectionView)를 업데이트한다.
    • 이러한 접근 방식은 코드의 모듈성과 재사용성을 높인다.
  • 유연성:
    • 이 방식을 사용하면 HomeViewController는 전체 테이블 뷰의 구조만 관리하고, 각 섹션(CollectionViewTableViewCell)은 자신의 내용을 독립적으로 관리할 수 있다.
    • 이는 향후 각 섹션에 대해 다른 데이터 소스를 사용하거나, 비동기적으로 데이터를 로드하는 등의 기능을 쉽게 구현할 수 있게 된다.
  • 불필요한 리로드 방지:
    • HomeViewController에서 전체 테이블을 리로드하면, 데이터가 변경되지 않은 다른 셀들도 불필요하게 리로드될 수 있다.
    • 현재 방식은 실제로 데이터가 변경된 특정 셀의 컬렉션 뷰만 리로드하므로 더 효율적이다.
  • 비동기 데이터 로딩 지원:
    • 각 섹션(CollectionViewTableViewCell)이 자신의 데이터를 독립적으로 로드하고 업데이트할 수 있어, 비동기 데이터 로딩 시나리오에 더 적합하다.

 

 

HomeViewController.swift 내에 API 함수를 구현한다. 

import UIKit

// 각 카테고리를 열거형으로 구현한다.
enum Sections: Int {
    case TrendingMovies = 0
    case TrendingTv = 1
    case Popular = 2
    case Upcoming = 3
    case TopRated = 4
}

class HomeViewController: UIViewController {
    
    // MARK: Variables
    let sectionTitles: [String] = ["Trending Movies", "Trending Tv", "Popular", "Upcoming Movies", "Top rated"]
    
    // MARK: UI Components
    private let homeFeedTable: UITableView = {
        let table = UITableView(frame: .zero, style: .grouped)
        table.register(CollectionViewTableViewCell.self, forCellReuseIdentifier: CollectionViewTableViewCell.identifier)
        return table
    }()
    ...
}

// MARK: Extensions
extension HomeViewController: UITableViewDelegate, UITableViewDataSource {
    
    // 테이블의 섹션 전체 수 
    func numberOfSections(in tableView: UITableView) -> Int {
        return sectionTitles.count
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 1
    }
    
    // 테이블에 섹션 별로 API 함수를 구현한다. 
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: CollectionViewTableViewCell.identifier, for: indexPath) as? CollectionViewTableViewCell else { return UITableViewCell() }
        
        switch indexPath.section {
        case Sections.TrendingMovies.rawValue:
            APICaller.shared.getTrendingMovies { result in
                switch result {
                case .success(let titles):
                    cell.configure(with: titles)
                case .failure(let error):
                    print(error.localizedDescription)
                }
            }
        case Sections.TrendingTv.rawValue:
            APICaller.shared.getTrendingTvs { result in
                switch result {
                case .success(let titles):
                    cell.configure(with: titles)
                case .failure(let error):
                    print(error.localizedDescription)
                }
            }
        case Sections.Popular.rawValue:
            APICaller.shared.getPopular { result in
                switch result {
                case .success(let titles):
                    cell.configure(with: titles)
                case .failure(let error):
                    print(error.localizedDescription)
                }
            }
        case Sections.Upcoming.rawValue:
            APICaller.shared.getUpcomingMovies { result in
                switch result {
                case .success(let titles):
                    cell.configure(with: titles)
                case .failure(let error):
                    print(error.localizedDescription)
                }
            }
        case Sections.TopRated.rawValue:
            APICaller.shared.getTopRated { result in
                switch result {
                case .success(let titles):
                    cell.configure(with: titles)
                case .failure(let error):
                    print(error.localizedDescription)
                }
            }
        default:
            return UITableViewCell()
        }
        return cell
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 200
    }
    
    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
        return 40 
    }
    
    // 테이블 뷰의 섹션 헤더가 화면에 표시되기 직전에 호출된다.
    func tableView(_ tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int) {
        guard let header = view as? UITableViewHeaderFooterView else { return }
        header.textLabel?.font = .systemFont(ofSize: 18, weight: .semibold)
        header.textLabel?.frame = CGRect(x: header.bounds.origin.x + 20, y: header.bounds.origin.y, width: 100, height: header.bounds.height)
        header.textLabel?.textColor = .label
        header.textLabel?.text = header.textLabel?.text?.capitalizeFirstLetter()
    }
    
    // 각 섹션의 타이틀을 불러온다.
    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return sectionTitles[section]
    }
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let defaultOffset = view.safeAreaInsets.top
        let offset = scrollView.contentOffset.y + defaultOffset
        
        navigationController?.navigationBar.transform = .init(translationX: 0, y: -offset)
    }
}