Concurrency: Download Image with Async/Await
세 가지 비동기 처리 방법인 @escaping, Combine, Async/Await을 통해 각각 이미지를 다운로드하는 예시비동기 처리를 위해 completionHandler를 사용하는 가장 전통적인 방법이다.\[weak self] 등의 방법으로 클로저 내에서 참조 타
Download Image with Async/Await
- 세 가지 비동기 처리 방법인 @escaping, Combine, Async/Await을 통해 각각 이미지를 다운로드하고 각각의 특징 비교

@escaping
- 비동기 처리를 위해
completionHandler를 사용하는 가장 전통적인 방법이다. [weak self]등의 방법으로 클로저 내에서 참조 타입에 대한 메모리 관리가 필요한다.- 별도에 프레임워크나 추가적인 도구 없이도 작업 가능하나, 복잡한 작업이나 에러 핸들링에서는 가독성이 떨어진다.
class DownloadImageAsyncImageLoader {
let url = URL(string: "https://picsum.photos/200")!
func handleResponse(data: Data?, response: URLResponse?) -> UIImage? {
guard
let data = data,
let image = UIImage(data: data),
let response = response as? HTTPURLResponse,
response.statusCode >= 200, response.statusCode < 300
else {
return nil
}
return image
}
func downloadWithEscaping(completionHandler: @escaping (_ image: UIImage?, _ error: Error?) -> Void) {
URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
let image = self?.handleResponse(data: data, response: response)
completionHandler(image, error)
}
.resume()
}
}
class DownloadImageAsyncViewModel: ObservableObject {
@Published var image: UIImage? = nil
let loader = DownloadImageAsyncImageLoader()
func fetchImage() {
loader.downloadWithEscaping { [weak self] image, error in
DispatchQueue.main.async {
self?.image = image
}
}
}
}
struct DownloadImageAsync: View {
@StateObject private var viewModel = DownloadImageAsyncViewModel()
var body: some View {
ZStack {
if let image = viewModel.image {
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(width: 240, height: 240)
}
}
.onAppear {
viewModel.fetchImage()
}
}
}
Combine
- 데이터의 흐름을 관찰하고 반응하여, 복잡한 비동기 코드를 더 선언적이고 쉽게 관리한다.
- 구독과 해제에 따라 메모리 관리가 자동으로 이루어진다.
- iOS 13 이상에서만 사용 가능하고 학습 곡선이 다소 높다.
class DownloadImageAsyncImageLoader {
let url = URL(string: "https://picsum.photos/200")!
func handleResponse(data: Data?, response: URLResponse?) -> UIImage? {
guard
let data = data,
let image = UIImage(data: data),
let response = response as? HTTPURLResponse,
response.statusCode >= 200, response.statusCode < 300
else {
return nil
}
return image
}
func downloadWithCombine() -> AnyPublisher<UIImage?, Error> {
URLSession.shared.dataTaskPublisher(for: url)
.map(handleResponse)
.mapError { $0 }
.eraseToAnyPublisher()
}
}
class DownloadImageAsyncViewModel: ObservableObject {
@Published var image: UIImage? = nil
let loader = DownloadImageAsyncImageLoader()
var cancellables = Set<AnyCancellable>()
func fetchImage() {
loader.downloadWithCombine()
.receive(on: DispatchQueue.main)
.sink { _ in
} receiveValue: { [weak self] image in
self?.image = image
}
.store(in: &cancellables)
}
}
struct DownloadImageAsync: View {
@StateObject private var viewModel = DownloadImageAsyncViewModel()
var body: some View {
ZStack {
if let image = viewModel.image {
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(width: 240, height: 240)
}
}
.onAppear {
viewModel.fetchImage()
}
}
}
Async/Await
- 최신 비동기 프로그래밍 패턴으로 비동기 코드를 마치 동기적인 코드처럼 작성하여 코드 가독성이 높고 복잡한 상황에서의 에러 핸들링이 용이하다.
Task와MainActor를 통해 비동기 작업의 우선순위 조절 및 UI 업데이트를 안전하게 관리할 수 있다.- 아직 새롭게 도입된 기능이므로 모든 프로젝트나 라이브러리에서 즉시 사용하기에는 제약사항이 있을 수 있다.
class DownloadImageAsyncImageLoader {
let url = URL(string: "https://picsum.photos/200")!
func handleResponse(data: Data?, response: URLResponse?) -> UIImage? {
guard
let data = data,
let image = UIImage(data: data),
let response = response as? HTTPURLResponse,
response.statusCode >= 200, response.statusCode < 300
else {
return nil
}
return image
}
func downloadWithAsync() async throws -> UIImage? {
do {
let (data, response) = try await URLSession.shared.data(from: url)
return handleResponse(data: data, response: response)
} catch {
throw error
}
}
}
class DownloadImageAsyncViewModel: ObservableObject {
@Published var image: UIImage? = nil
let loader = DownloadImageAsyncImageLoader()
var cancellables = Set<AnyCancellable>()
func fetchImage() async {
let image = try? await loader.downloadWithAsync()
await MainActor.run {
self.image = image
}
}
}
struct DownloadImageAsync: View {
@StateObject private var viewModel = DownloadImageAsyncViewModel()
var body: some View {
ZStack {
if let image = viewModel.image {
Image(uiImage: image)
.resizable()
.scaledToFit()
.frame(width: 240, height: 240)
}
}
.onAppear {
Task {
await viewModel.fetchImage()
}
}
}
}