감자주먹밥

[IOS] 영화 앱 - Swift Concurrency 직접 적용하기 본문

IOS/UIKit

[IOS] 영화 앱 - Swift Concurrency 직접 적용하기

JustHm 2023. 3. 23. 22:50
728x90

저번에 UICollectionView의 Compositional Layout을 공부할 때 만들었던 영화앱의 API 통신 부분을 공부할 겸 변경하게 되었다.

영화진흥원 API를 사용해 일간, 주간 박스 오피스 리스트를 가져오고, KMDB API를 사용해 영화 상세정보를 받아와 포스터를 띄웠다.

///BoxOffice 일간, 주간 정보 모두 반환
func searchBoxOfficeInfo(dateRange: DateRange) async throws -> [MovieInfo] {
    let url = APIInfo.boxOfficeHost + dateRange.rawValue
    var dataTask: DataTask<BoxOfficeJSON>
    switch dateRange {
    case .daily:
        let param = BoxOfficeRequestModel(key: getAPIKey(hostName: .kobis), targetDt: Date().dailyDate)
        dataTask = AF.request(url, method: .get, parameters: param).serializingDecodable(BoxOfficeJSON.self)
    case .weekly:
        let param = BoxOfficeRequestModel(key: getAPIKey(hostName: .kobis), targetDt: Date().weeklyDate)
        dataTask = AF.request(url, method: .get, parameters: param).serializingDecodable(BoxOfficeJSON.self)
    }
    
    switch await dataTask.result {
    case .success(let data):
        return dateRange == .daily ? data.boxOfficeResult.dailyBoxOfficeList! : data.boxOfficeResult.weeklyBoxOfficeList!
    case .failure(let error):
        print(error.localizedDescription)
        throw error
    }
}
///영화 상세 정보 
func searchMovieInfo(title: String, releaseDate: String?) async throws -> Movie? {
    let url = APIInfo.movieDetailHost
    var date: String {
        if let temp = releaseDate {
            return temp.replacingOccurrences(of: "-", with: "")
        }
        else {
            return ""
        }
    }
    let param: Parameters = ["ServiceKey": getAPIKey(hostName: .kmdb),
                             "collection": "kmdb_new2",
                             "detail": "Y",
                             "title": title,
                             "releaseDts": date]
    let dataTask = AF.request(url, method: .get, parameters: param).serializingDecodable(MovieDetailJSON.self)
    switch await dataTask.result {
    case .success(let data):
        guard let data = data.data[0].result else {
            print("가져오기 실패 [\(title)] : \(data.data)")
            return nil
        }
        return data[0]
    case .failure(let error):
        throw error
    }
}

Request 함수는 Alamofire를 사용해 구현했다. Alamofire에서도 async/await를 지원하는 함수가 있어 그걸 그대로 사용하니 간단했다.

이전에 열심히 escaping closure를 사용했을 때 보다 코드도 간결하고 깔끔해졌다.

 

실제로 데이터를 사용하는 부분의 구현은 동시성, 병렬처리가 가능하다는 장점을 살리고 싶어, async let syntax를 일간, 주간 박스오피스를 가져올 때 사용해보고, 박스오피스 영화들의 정보를 가져올 땐 task group을 사용해 구현했다.

private func getDataAsync() {
    Task {
        do {
            async let daily = APIManager().searchBoxOfficeInfo(dateRange: .daily)
            async let weekly = APIManager().searchBoxOfficeInfo(dateRange: .weekly)
            let movies = try await [daily, weekly]
            //BoxOffice 일간 주간 리스트
            dailyList = movies[0]
            weeklyList = movies[1]
            //boxOfficeInfo에서 영화이름을 Key로 포스터를 가져오거나 상세 정보를 표시.
            boxOfficeInfo = try await loadMovieDetail(movies: dailyList + weeklyList)
            collectionView.reloadData()
        } catch {
            fatalError(error.localizedDescription)
        }
    }
}
private func loadMovieDetail(movies: [MovieInfo]) async throws -> [String: Movie] {
    var temp: [String: Movie] = [:]
    try await withThrowingTaskGroup(of: (String, Movie?).self) { group in
        for movie in (dailyList + weeklyList) {
            group.addTask {
                return try await (movie.movieNm,
                                  APIManager().searchMovieInfo(title: movie.movieNm, releaseDate: movie.openDt))
            }
        }
        for try await (name, movie) in group {
            guard let movie else { continue }
            temp[name] = movie
        }
    }
    return temp
}

Swift Concurrency 영상에서 처럼 썸네일을 가져오는 코드를 비슷하게 구현했다. 정확히는 영화의 상세정보를 가져오는 것이지만, async-let과 달리 많고, 가변적인 상황에서는 taskgroup으로 처리하는게 정말 간편하다.

또한 ViewController에서 작업하면 Task가 MainActor를 따르기 때문에 메인스레드에 따로 UI를 조작하는 번거로움도 사라지고 이 부분이 코드가 가장 간결해졌다.

아쉽게도 KMDB에서 제공하는 영화상세정보 API가 영화이름과 개봉일로 검색해도 제대로 검색이 안되어서 이미지가 로드가 안 된 것들이 많다.

재개봉의 경우 "|"를 구분자로 사용하는데 검색시 대응이 안되는 문제도 있고 이번에 개봉한 "웅남이"도 API에서 이름으로만 검색해도 안나오는 알 수 없는 문제가 있다... 그냥 그런대로 써야겠다.

원래는 MVVM + RxSwift로 변경하려고 했던 프로젝트였지만, 이걸 적용해보게 됐다.. Rx는 왜 어려울까

728x90
Comments