Concurrency: Download Image with Async/Await

Snack 남관식·2023년 8월 8일

Concurrency

목록 보기
1/2
post-thumbnail

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

  • 최신 비동기 프로그래밍 패턴으로 비동기 코드를 마치 동기적인 코드처럼 작성하여 코드 가독성이 높고 복잡한 상황에서의 에러 핸들링이 용이하다.
  • TaskMainActor를 통해 비동기 작업의 우선순위 조절 및 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()
            }
        }
    }
}
profile
iOS Developer | Product Designer @snacknam

0개의 댓글