Using Combine for Your App’s Asynchronous Code

Horus-iOS·2023년 10월 23일
0

https://developer.apple.com/documentation/combine/using-combine-for-your-app-s-asynchronous-code

Apply common patterns to migrate your closure-based, event-handling code.

클로저 기반, 이벤트 처리 코드를 일반적인 패턴을 적용해서 마이그레이션합니다.

Overview

비동기 처리 시 다음처럼 일반적인 패턴을 사용하고 있을 것입니다.

  • 컴플리션 핸들러, 호출하는 쪽이 한 번 실행하기 위한 클로저를 제공한 후 잠재적으로 긴 시간 동안 작업이 수행되는 경우입니다.
  • 클로저 속성, 호출자가 주어진 비동기 이벤트가 발생할 때마다 호출하기 위해 클로저를 제공하는 경우입니다.

컴바인은 이와 같은 패턴과 동일한 기능을 제공함으로써 보일러 플레이트 코드를 제공하고 여러 오퍼레이터를 활용할 수 있게 해줍니다. 앱에서 컴바인을 채택하면 비동기 호출 지점을 컴바인으로 전환함으로써 코드의 일관성과 가독성을 향상시킬 수 있습니다.

Tip
async, await 기능을 사용하고 있다면 클로저 기반 비동기 패턴을 사용할 필요가 없습니다. 대신 코드는 비동기 호출을 기다리고, 이후 클로저에 담길 코드를 실행할 수 있습니다. 이로써 전통적인 컴플리션 핸들러 및 컴바인 기능의 필요성이 없어집니다. 더 많은 정보는 'The Swift Programming Language'에 있는 'Concurrency'를 보시기 바랍니다.

Concurrency
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/

Replace Completion-Handler Closures with Futures

컴플리션 핸들러는 작업을 완전히 마친 후 실행되는 함수에 의해 수용되는 클로저입니다. 보통 컴플리션 핸들러를 직접 구현해 호출함으로써 언제 함수가 작업을 완료하는지 파악하고, 필요에 따라 함수 밖에 클로저를 저장하기도 할 것입니다. 예를 들어 아래 함수는 클로저를 받고 있고, 2초 딜레이 후 클로저가 실행합니다.

func performAsyncAction(completionHandler: @escaping () -> Void) {
    DispatchQueue.main.asyncAfter(deadline:.now() + 2) {
        completionHandler()
    }
}

이와 같은 패턴은 컴바인의 Future로 대체할 수 있습니다. Future는 특정 작업을 수행하고 성공, 실패 여부를 비동기 신호로 보내는 퍼블리셔입니다. 성공하는 경우 Future.Promise를 실행합니다. Future.Promisefuture가 제공하는 요소를 받는 클로저입니다. 이전에 소개한 함수를 아래처럼 바꿀 수 있습니다.

func performAsyncActionAsFuture() -> Future <Void, Never> {
    return Future() { promise in
        DispatchQueue.main.asyncAfter(deadline:.now() + 2) {
            promise(Result.success(()))
        }
    }
}

작업 수행이 완료됐을 때 클로저를 명시적으로 호출하는 것이 아니라 future는 전달되는 promise를 호출하고 성공, 실패 여부를 나타내는 Result를 전달합니다. 호출자는 future로부터 이 결과를 비동기로 받습니다. Future는 컴바인의 퍼블리셔이기 때문에 호출자는 선택에 따라 오퍼레이터의 연쇄를 실행할 수 있으며, sink(receiveValue:)와 같은 구독자로 종료시킬 수 있습니다.

cancellable = performAsyncActionAsFuture()
    .sink() { _ in print("Future succeeded.") }

Use Output Types to Represent a Future’s Parameters

간혹 장시간 수행하는 작업은 컴플리션 핸들러에 파라미터로 전달하는 값을 생성하는 경우가 있습니다. 컴바인에서 이처럼 사용하려면 future가 전달하는 출력 타입으로 파라미터를 선언해서 해결할 수 있습니다. 아래 예시는 무작위로 생성되는 인티저를 제공하고 있고, Intfuture의 출력 타입으로 선언해서 promise로 전달하는 메소드입니다.

func performAsyncActionAsFutureWithParameter() -> Future <Int, Never> {
    return Future() { promise in
        DispatchQueue.main.asyncAfter(deadline:.now() + 2) {
            let rn = Int.random(in: 1...10)
            promise(Result.success(rn))
        }
    }
}

futureInt요소를 제공하도록 선언했기 때문에 futureInt 값을 promise로 전달하는 Result 타입을 사용할 수 있습니다. promise가 실행될 때 future는 호출자가 sink(receiveValue:)와 같은 구독자를 사용해서 받을 수 있도록 합니다.

cancellable = performAsyncActionAsFutureWithParameter()
    .sink() { rn in print("Got random number \(rn).") }

Replace Repeatedly Invoked Closures with Subjects

특정 이벤트가 발생했을 때 호출하기 위한 클로저를 속성으로써 사용하는 패턴도 있을 수 있습니다. 이 속성은 보통 on으로 시작하는 네이밍을 갖고 있을 것이며, 호출 시점은 아래와 같을 것입니다.

vc.onDoSomething = { print("Did something.") }

컴바인을 사용하면 Subject를 사용해서 대체할 수 있습니다. subjectsend()를 호출할 때마다 새로운 요소를 내보낼 수 있습니다. private PassthroughSubject 혹은 private CurrentValueSubject를 사용해서 AnyPublisher로 내보낼 수 있습니다.

private lazy var myDoSomethingSubject = PassthroughSubject<Void, Never>()
lazy var doSomethingSubject = myDoSomethingSubject.eraseToAnyPublisher()

클로저 속성을 설정하지 않고 위와 같은 정렬을 사용하면 호출자는 sink(receiveValue:)와 같은 구독자 코드 안에서 작업을 수행할 수 있습니다.

cancellable = vc.doSomethingSubject
    .sink() { print("Did something with Combine.") }

컴바인을 사용하는 것의 한 가지 이점은 subject가 구독자에게 send(completion:)를 호출해서 이벤트가 없거나 오류가 발생했음을 알려줄 수도 있다는 점입니다.

Tip
async, await 기능을 사용하고 있다면 컴바인의 Subject 대신 AsyncStream을 사용해서 비동기로 새로운 요소를 제공할 수 있습니다. 호출 시점은 for-await-in 루프를 수행해서 subject의 구독 시점이 아닌, stream으로써 값 제공을 반복해서 받습니다. 구독자의 receiveValue 클로저가 담긴 코드 대신 for-await-in의 컨텐츠가 될 것입니다.

0개의 댓글