async let을 사용하면 여러 데이터를 한번에 처리할 수 있습니다
https://apple-document.tistory.com/170
지난번에 이미지를 예를 들어 여러 이미지를 가져올 때 딜레이를 없애고 여러 이미지를 동시에 가져오는 방법인 async let을 알아봤었습니다. 이번에는 실제로는 어떻게 사용되었는지 코드를 분석하면서 알아볼 것입니다.
사용 사례
박스오피스 영화 데이터를 가져오기 위해서는 한 개가 아닌 여러개의 영화 정보를 가져와야 할 것입니다.
만약 async let을 사용하지 않는다면 다음과 같은 현상을 마주하게 될 것입니다.
async let 사용 전
코드는 깊게 설명하지 않고 어떤 식으로 사용되는지 느낌만 알아보겠습니다.
- 첫 번째로 fetchData를 사용하여 영화 데이터들을 가져옵니다
- 영화 데이터에는 제목 뿐만 아니라 이미지 링크도 포함되어 있습니다 - 두 번째로 가져온 영화 데이터(배열)을 for 문으로 반복하여 performRequest를 통해 얻은 이미지를 포함한 해당 영화에 대한 정보들을 저장합니다
확인해보면 영화 데이터를 가져오는대로 데이터를 movies 에 append 하여 삽입하기 때문에 데이터가 실시간으로 반영됩니다.
이런 동작은 스무스한 앱을 만들 수 없습니다.
var movies: [Movie] = []
func fetchMovies() {
Task {
do {
guard let moviesDTO = try await networkManager.fetchData(to: MoviesDTO.self, endPoint: popularEndPoint) as? MoviesDTO else { return }
let movieList = moviesDTO.movies
for movie in movieList {
let title = movie.title
let id = movie.id
guard let posterPath = movie.posterPath else { return }
let imageEndPoint = MovieImageAPIEndPoint(imageURL: posterPath)
let imageResult = try await networkFetcher.performRequest(imageEndPoint.urlRequest)
switch imageResult {
case .success(let data):
guard let posterImage = UIImage(data: data) else { return }
let movie = Movie(id: id, title: title, posterImage: posterImage)
movies.append(movie)
case .failure(let error):
print(error)
}
}
}
}
}
물론 async let을 사용하지 않고도 원하는 동작을 수행하는 코드를 만들 수 있습니다.
var movies: [Movie] = []
func fetchMovies() {
Task {
do {
guard let moviesDTO = try await networkManager.fetchData(to: MoviesDTO.self, endPoint: popularEndPoint) as? MoviesDTO else { return }
let movieList = moviesDTO.movies
var movies: [Movie] = []
for movie in movieList {
let title = movie.title
let id = movie.id
guard let posterPath = movie.posterPath else { return }
let imageEndPoint = MovieImageAPIEndPoint(imageURL: posterPath)
let imageResult = try await networkFetcher.performRequest(imageEndPoint.urlRequest)
switch imageResult {
case .success(let data):
guard let posterImage = UIImage(data: data) else { return }
let movie = Movie(id: id, title: title, posterImage: posterImage)
movies.append(movie)
case .failure(let error):
print(error)
}
}
self.movies = movies
}
}
}
async let 사용 후
위에 문제를 async let을 사용한다면 해결할 수 있습니다.
- 위와 동일하게 fetchData 를 통해서 영화들의 데이터를 가져옵니다.
- 가져온 영화들을 UI에 띄우기 위해 지역변수인 movies 를 하나 만들어줍니다.
- 영화에 대한 이미지를 비동기적으로 가져오기 위해 Task 배열을 만들어줍니다.
- Task 는 Movie와 UIImage 데이터를 튜플로 가지고 있으며 절대 실패하지 않는 것을 보장하는 Never를 사용합니다. - for문을 반복하며 Task 배열에 API로부터 받은 Movie와 이미지를 return 해줍니다.
- 이후 반환 받은 값을 imageTasks에 추가해줍니다
- 데이터를 담을 수 있는 movieResults 를 만들어 줍니다.
- imageTasks 에 담겨있는 데이터를 for 문으로 데이터를 추가해줍니다
- 추가한 데이터를 또 for 문을 이용하여 지역변수 movies에 추가하고 전역변수에 담아 UI에 반영될 수 있도록 합니다.
var movies: [Movie] = []
func fetchMovies() {
Task {
do {
//1.위와 동일하게 fetchData 를 통해서 영화들의 데이터를 가져옵니다.
guard let moviesDTO = try await networkManager.fetchData(to: MoviesDTO.self, endPoint: popularEndPoint) as? MoviesDTO else { return }
//2.가져온 영화들을 UI에 띄우기 위해 지역변수인 movies 를 하나 만들어줍니다.
let movieList = moviesDTO.movies
var movies: [Movie] = []
// 모든 영화에 대한 이미지를 비동기적으로 가져오기 위해 async let 사용
var imageTasks: [(movie: Movie, posterImageTask: UIImage?)] = []
for movie in movieList {
let title = movie.title
let id = movie.id
if let posterPath = movie.posterPath {
let imageEndPoint = MovieImageAPIEndPoint(imageURL: posterPath)
// async let을 사용하여 비동기적으로 이미지 다운로드
async let posterImageTask: UIImage? = {
if let imageResult = try? await networkFetcher.performRequest(imageEndPoint.urlRequest),
case .success(let data) = imageResult {
return UIImage(data: data)
}
return nil
}()
// (movie: Movie, posterImageTask: UIImage?) 타입인 imageTasks에 추가
await imageTasks.append((Movie(id: id, title: title, posterImage: nil), posterImageTask))
} else {
// 이미지 링크가 존재하지 않는다면 다음과 nil값으로 반환
imageTasks.append((Movie(id: id, title: title, posterImage: nil), nil))
}
}
// 모든 이미지 다운로드 완료 대기 및 movies 배열 업데이트
for (movie, posterImageTask) in imageTasks {
let posterImage = posterImageTask
if let posterImage = posterImage {
movies.append(Movie(id: movie.ID, title: movie.title, posterImage: posterImage))
} else {
print("Failed to load image for movie id: \(movie.id)")
}
}
// 한꺼번에 UI에 반영
self.movies = movies
} catch {
print("Failed to fetch movies: \(error)")
}
}
}
위에 작업을 2개의 함수로 만들어서 분할하여 가독성을 좋게 만들 수도 있습니다.
func fetchMovies() {
Task {
do {
guard let moviesDTO = try await networkManager.fetchData(to: MoviesDTO.self, endPoint: popularEndPoint) as? MoviesDTO else { return }
let movieList = moviesDTO.movies
var movies: [Movie] = []
// 비동기 이미지 다운로드를 위한 async let
for movie in movieList {
async let movieWithImage = fetchMovieWithImage(movie)
let result = try await movieWithImage
movies.append(result)
}
self.movies = movies
} catch {
print("Failed to fetch movies: \(error)")
}
}
}
func fetchMovieWithImage(_ movie: MovieDTO) async throws -> Movie {
let title = movie.title
let id = movie.id
guard let posterPath = movie.posterPath else {
throw NetworkError.invalidURL
}
let imageEndPoint = MovieImageAPIEndPoint(imageURL: posterPath)
let imageResult = try await networkFetcher.performRequest(imageEndPoint.urlRequest)
switch imageResult {
case .success(let data):
guard let posterImage = UIImage(data: data) else {
throw NetworkError.emptyData
}
return Movie(id: id, title: title, posterImage: posterImage)
case .failure(let error):
throw error
}
}
개인적 생각
async let에 관해 얘기를 했기 때문에 async let을 사용했지만 개인적인 생각으로는 위에 코드같은 경우 async let 을 사용하기 보다는 아래 async let을 사용하지 않는 것이 코드도 간결해지고 사용하는 입장에서도 더 좋다고 생각이 들었습니다.
var movies: [Movie] = []
func fetchMovies() {
Task {
do {
guard let moviesDTO = try await networkManager.fetchData(to: MoviesDTO.self, endPoint: popularEndPoint) as? MoviesDTO else { return }
let movieList = moviesDTO.movies
var movies: [Movie] = []
for movie in movieList {
let title = movie.title
let id = movie.id
guard let posterPath = movie.posterPath else { return }
let imageEndPoint = MovieImageAPIEndPoint(imageURL: posterPath)
let imageResult = try await networkFetcher.performRequest(imageEndPoint.urlRequest)
switch imageResult {
case .success(let data):
guard let posterImage = UIImage(data: data) else { return }
let movie = Movie(id: id, title: title, posterImage: posterImage)
movies.append(movie)
case .failure(let error):
print(error)
}
}
self.movies = movies
}
}
}
'SwiftUI' 카테고리의 다른 글
swiftUI - 간단하게 검색창 만들기 (0) | 2024.06.15 |
---|---|
SwiftUI - 긴 글자 Text에서 일부만 표현하기 (with truncationMode) (0) | 2024.06.13 |
통신을 위한 URLComponents 구성하기 (2) | 2024.06.11 |
SwiftUI - 설정창을 만들어 언어 설정하기 (3) | 2024.06.08 |
Observable Macro 에서 할 수 있는 실수 (Thread 문제) (0) | 2024.06.06 |