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()
  }
}

참고