async let을 사용하면 여러 데이터를 한번에 처리할 수 있습니다

 

https://apple-document.tistory.com/170

 

async let 으로 비동기 작업들을 동시에 수행하기 (1)

async let 을 사용하면 시스템이 async let  오른쪽에 있는 비동기 함수를 병렬적으로 실행시킵니다   await를 사용할 때는 시스템이 해당 비동기 함수를 실행시킬 때마다 실행을 담당하는 스레드를

apple-document.tistory.com

 

지난번에 이미지를 예를 들어 여러 이미지를 가져올 때 딜레이를 없애고 여러 이미지를 동시에 가져오는 방법인 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을 사용한다면 해결할 수 있습니다.

 

  1. 위와 동일하게 fetchData 를 통해서 영화들의 데이터를 가져옵니다.
  2. 가져온 영화들을 UI에 띄우기 위해 지역변수인 movies 를 하나 만들어줍니다.
  3. 영화에 대한 이미지를 비동기적으로 가져오기 위해 Task 배열을 만들어줍니다.
    - Task 는 Movie와 UIImage 데이터를 튜플로 가지고 있으며 절대 실패하지 않는 것을 보장하는 Never를 사용합니다.
  4. for문을 반복하며 Task 배열에 API로부터 받은 Movie와 이미지를 return 해줍니다.
  5. 이후 반환 받은 값을 imageTasks에 추가해줍니다
  6. 데이터를 담을 수 있는 movieResults 를 만들어 줍니다.
  7. imageTasks 에 담겨있는 데이터를 for 문으로 데이터를 추가해줍니다
  8. 추가한 데이터를 또 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
        }
    }
}

 

 

ytw_developer