저번에 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는 왜 어려울까