Combine에서 async/await로 변경하기

  • Swift 5.5에 async/await가 등장하면서, 많은 것을 대체하게 되었는데요.
  • 그 중 대표적인 것이 비동기 프로그래밍을 도와주던 Closure와 Combine입니다.

Async/await

  • 공식 문서를 보면, 다음과 같은 팁이 적혀있습니다.
  • 대략, async/awaitcompletion 클로저와 Combine의 필요성을 없앤다고 합니다.

You don’t need closure-based asychronicity patterns if you’re using the async-await features in Swift 5.5 and later. Instead, your code can await an asynchronous call, and then execute the code that would have been in the closure. This eliminates the need for both conventional completion handlers and Combine futures. For more information, see Concurrency in The Swift Programming Language.

Example

  • 다음과 같이 Combine으로 구현 된 3가지 예시 파일 API, Service, ViewModel이 있습니다.
  • 각각의 함수 선언부와 리턴부만 표기해보겠습니다.
// Combine 구현

// API.swift
class API {
  func perform<T: Decodable>(method: HTTPMethod, url: String?) -> AnyPublisher<HTTPResponse<T>, Error> {
    ...

    return URLSession.shared.dataTaskPublisher(for: request)
      .tryMap { result -> HTTPResponse<T> in
        let value = try self.decoder.decode(T.self, from: result.data)
        return HTTPResponse(value: value, response: result.response)
      }
      .receive(on: DispatchQueue.main)
      .eraseToAnyPublisher()
  }
}

// Service.swift
class Service: ServiceType {
  ...

  func request() -> AnyPublisher<[SomeType], Error> {
    ...

    return API.shared.perform(method: .get, url: component?.string)
      .map(\.value)
      .eraseToAnyPublisher()
  }
}

// ViewModel.swift
func requestSomething() {
  self.service.request()
    .sink { _ in
      ...
    } receiveValue: { response in
      self.response = response
    }.store(in: &self.cancellable)
}

API.swift

  • API.swift 변경점 입니다.
  • 우선 함수 선언부에서 AnyPublisher를 제거하고 async throws를 추가했습니다.
// Combine
func perform<T: Decodable>(method: HTTPMethod, url: String?) -> AnyPublisher<HTTPResponse<T>, Error>

// Async/await
func perform<T: Decodable>(method: HTTPMethod, url: String?) async throws -> HTTPResponse<T>
  • 그에 맞게 return 타입도 변경해줍니다.
  • dataTaskPublisher 대신 async 메소드인 data(from:)을 사용합니다.
// Combine
return URLSession.shared.dataTaskPublisher(for: request)
  .tryMap { result -> HTTPResponse<T> in
      let value = try self.decoder.decode(T.self, from: result.data)
      return HTTPResponse(value: value, response: result.response)
  }
  .receive(on: DispatchQueue.main)
  .eraseToAnyPublisher()

// Async/await
let (data, urlResponse) = try await URLSession.shared.data(from: url)
let response = try decoder.decode(T.self, from: data)
return HTTPResponse(value: response, response: urlResponse)

Service.swift

  • Service.swift 변경점 입니다.
  • 똑같이 AnyPublisher를 제거하고 async throws를 추가했습니다.
// Combine
func request() -> AnyPublisher<[SomeType], Error> {}

// Async/await
func request() async throws -> [SomeType]
  • 역시나 그에 맞게, return을 수정해줍니다.
// Combine
return API.shared.perform(method: .get, url: component?.string)
  .map(\.value)
  .eraseToAnyPublisher()

// Async/await
return try await API.shared.perform(method: .get, url: component?.string).value

ViewModel.swift

  • ViewModel.swift 변경점 입니다.
  • sink로 값을 받아오던 것과는 다르게 Task로 값을 받아옵니다.
// Combine
func requestSomething() {
  self.service.request()
    .sink { _ in
      ...
    } receiveValue: { response in
      self.response = response
    }.store(in: &self.cancellable)
}

// Async/await
func requestSomething()  {
  Task {
    self.response = try await self.service.request()
  }
}
  • 다음과 같은 방식으로 호출하면 되겠습니다.
View()
  .onAppear {
    self.viewModel.requestSomething()
  }

결과물

  • Combine 대신 async/await를 사용해서 구현해본 3가지 파일의 결과물입니다.
  • 개인적으로는 훨씬 직관적이고 간단해졌다고 생각합니다.
// Async/await 구현

// API.swift
class API {
  func perform<T: Decodable>(method: HTTPMethod, url: String?) async throws -> HTTPResponse<T> {
    ...

    let (data, urlResponse) = try await URLSession.shared.data(from: url)
    let response = try decoder.decode(T.self, from: data)
    return HTTPResponse(value: response, response: urlResponse)
  }
}

// Service.swift
class Service: ServiceType {
  ...

  func request() async throws -> [SomeType]
    ...

    return try await API.shared.perform(method: .get, url: component?.string).value
  }
}

// ViewModel.swift
func requestSomething()  {
  Task {
    self.response = try await self.service.request()
  }
}

참고