본문 바로가기

Project/MovieClip

TMDB API 데이터를 가져오는 방식 - async / await 방식 채택

 

 

📌 TMDB API 데이터를 가져오는 3가지 방식

  1. Completion Handler (기존 방식) 
  2. Combine (React 프로그래밍)
  3. async/await (Swift Concurrency)

 

📍 이전에 NetFlix를 클론 코딩했을 때는 Completion Handler 를 사용했습니다. 

 

✅ 1. Completion Handler 방식 (기존 방식)

비동기 요청이 완료된 후 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 {
            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()
}

 

🔹 장점

✅ 간단하고, 기존 코드와의 호환성이 좋음
✅ iOS 13 이하에서도 사용 가능

🔹 단점

⚠️ 콜백 중첩 문제 (Callback Hell) 발생 가능
⚠️ 코드 가독성이 떨어짐

 

 

✅ 2. Combine 방식

Combine을 사용하면 데이터 흐름을 스트림 형태로 관리

Future 또는 AnyPublisher를 사용하여 비동기적으로 데이터를 반환하는 방식

import Combine

class MovieService {
    private var cancellables = Set<AnyCancellable>()
    
    func getTrendingMovies() -> AnyPublisher<[Title], Error> {
        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)"
        ]
        
        return URLSession.shared.dataTaskPublisher(for: request)
            .tryMap { data, response -> Data in
                guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
                    throw APIError.failedToGetData
                }
                return data
            }
            .decode(type: TrendingTitleResponse.self, decoder: JSONDecoder())
            .map { $0.results }
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
}

🔹 사용 방법

let movieService = MovieService()
movieService.getTrendingMovies()
    .sink(receiveCompletion: { completion in
        switch completion {
        case .failure(let error):
            print("Error: \(error)")
        case .finished:
            print("Successfully fetched movies")
        }
    }, receiveValue: { movies in
        print("Movies: \(movies)")
    })
    .store(in: &cancellables)

🔹 장점

반응형 프로그래밍 가능 (데이터가 변경될 때 UI 업데이트 가능)
콜백 중첩 문제 해결
데이터 스트림을 쉽게 관리할 수 있음

🔹 단점

⚠️ iOS 13 이상에서만 사용 가능
⚠️ Combine을 잘 이해해야 사용 가능
⚠️ 메모리 관리 필요 (store(in: &cancellables))

 

 

✅ 3. async/await 방식 (Swift Concurrency)

iOS 15 이상에서는 async/await을 사용하여 가독성이 뛰어난 비동기 처리가 가능

🔹 코드 

class NetworkManager {
    
    static let shared = NetworkManager()
        
    func getTrendingMovies() async throws -> [MovieResult] {
        let url = URL(string: "\(Constants.baseURL)trending/movie/week")!
        var components = URLComponents(url: url, resolvingAgainstBaseURL: true)!
        let queryItems: [URLQueryItem] = [
          URLQueryItem(name: "language", value: "ko-KR"),
        ]
        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 (data, response) = try await URLSession.shared.data(for: request)
        
        guard let httpResponse = response as? HTTPURLResponse,
              httpResponse.statusCode == 200 else { throw APIError.failedToGetData }
        
        let results = try JSONDecoder().decode(MovieWelcome.self, from: data)
        
        return results.results
    }
}

🔹 사용 방법

Task {
    do {
        let movies = try await getTrendingMovies()
        print("Movies: \(movies)")
    } catch {
        print("Error: \(error)")
    }
}

 

🔹 장점

코드 가독성이 뛰어남
비동기 처리가 간결해짐
콜백 중첩 문제 해결

🔹 단점

⚠️ iOS 15 이상에서만 사용 가능
⚠️ 기존 코드와 호환성이 떨어질 수 있음

 

 

🚀 어떤 방식을 선택해야 할까?

iOS 13 이하까지 지원해야 한다면Completion Handler
Combine을 이미 사용하고 있다면Combine 방식
iOS 15 이상에서 개발 중이라면async/await 

 

📍 async / await 방식 채택 (한 번도 사용해보지 않았기 때문이고, 가독성이 좋아서 채택)

 

 

💻 에러 처리 강화 관련 APIERROR 확장 

🔷 기존 

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

 

🔷 개선

enum APIError: Error {
    case invalidURL                         // URL이 잘못된 경우 (강제 언래핑 방지)
    case requestFailed(statusCode: Int)     // HTTP 상태 코드가 200이 아닐 때
    case decodingFailed(Error)              // JSON 디코딩 실패 시
    case networkError(Error)                // 네트워크 에러 발생 시
    case unknownError                       // 알 수 없는 에러 발생 시
}