WWDC/iOS

[iOS] WWDC19 - Combine in Practice (2)

유정주 2023. 1. 22. 23:29
반응형

이전 포스팅

2023.01.22 - [🍎 iOS/iOS 개념&개발] - [iOS] WWDC19 - Combine in Practice (1)

 

[iOS] WWDC19 - Combine in Practice (1)

Combine Combine은 시간의 흐름에 따라 값을 처리하는 API 입니다. 자세한 내용은 이전 포스팅인 "WWDC19 - Introducing Combine"을 참고해 주세요. 이번 발표인 "WWDC19 - Combine in Practice"에서는 실제로 Combine을

jeong9216.tistory.com

 

Subscriber

지난 포스팅에서 Publisher에 대해 알아보았으니,

이제는 Subscriber에 대해 알아봅시다.

 

Subscriber는 Publisher와 동일하게 두 개의 associatedType으로 구성되었습니다.

 

Subscriber는 세 개의 receive 메서드를 가지고 있는데요.

이 세 개의 메서드는

recieve(subscription:) -> receive(_ value:) -> receive(completion:) 순서대로 호출됩니다.

Publisher와 Subscriber가 연결될 때 receive(subscription:)이 호출됩니다.

이 메서드는 딱 한 번만 호출이 됩니다.

 

receive(_:)는 데이터가 전달되는 메서드로,

Subscriber가 Publisher에게 0개 이상의 값을 요청했을 때 downstream를 통해 제공됩니다.

 

마지막으로 receive(completion:)은 Publisher의 완료 신호로,

모든 값이 방출됐을 때 Subscriber에게 전달되는 메서드입니다.

 

이때 에러가 발생하면,

Publisher와 Subscriber의 연결이 끊기게 됩니다.

 

정리하면,

Subscriber는 Publisher를 구독했을 때 하나의 Subscription을 전달 받고,

값을 요청하면 0개 이상의 값을 전달 받을 수 있습니다.

마지막으로 최대 한 번의 완료 신호 혹은 실패 신호를 completion을 통해 전달 받습니다.

이때, 완료 신호는 옵셔널이므로 많은 stream에서 종료되지 않고 계속 동작할 수도 있습니다. (like NotificationCenter)

 

Kinds of Subscribers

Combine에서는 다양한 종류의 Subscriber를 지원합니다.

 

Key Path Assignment

Key Path Assignment는 Combine에서 가장 간단한 Subscriber 입니다.

Key Path Assignment는 assign(to:on:) Operator를 사용하고,

이 함수는 Publisher가 방출한 값이 특정 Key Path에 할당된 객체에 전달됩니다.

위 코드에서는 Publisher로부터 받은 값을 someObject의 someProperty에 전달합니다.

 

assign 오퍼레이터도 cancellation 토큰을 생성해서 subscription을 종료할 수 있습니다.

 

cancellation

그럼 cancellation이란 뭘까요?

Cancellation은 Combine에 포함되는 기능으로,

Publisher가 이벤트를 전달하기 전에 Subscription을 종료할 수 있습니다.

Cancellation은 subscription과 관련된 리소스를 해제할 때 유용합니다.

 

AnyCacnellable 프로토콜은 deinit 단계에서 자동으로 cencel을 해주는 유용한 프로토콜입니다.

Swift에게 메모리 관리를 맡기고, 개발자가 직접 cancel을 호출하지 않아도 됩니다.

 

Sink

두 번째로 알아볼 Subscription은 sink 입니다.

sink를 이용해 전달 받는 값에 대한 클로저를 등록하면,

새로운 값이 전달될 때마다 해당 클로저가 실행됩니다.

 

sink는 cancellable을 반환해서, 이를 이용해 subscription을 종료시킬 수도 있습니다.

 

Subjects

세 번째로 알아볼 Subjects는 Publisher와 Subscriber가 혼합된 유형입니다.

멀티캐스팅을 지원하고, 값을 필수적으로 전송하게 합니다.

 

Subject를 이용하면 여러 Subscriber에게 동시에 값을 방출할 수 있습니다.

send(_:)를 이용한 값 전달도 가능하고,

Publisher를 이용한 값 방출도 동시에 가능합니다.

 

Combine은 두 개의 Subject를 지원하는데요.

Passthrough는 값을 저장하지 않아서 subscribe 하지 않으면 값을 확인할 수 없고,

CurrentValue는 마지막으로 받았던 값을 기록해서 새로운 Subscriber가 연결되어도 값을 계속 추적할 수 있습니다.

 

이제 어떻게 사용하는지 봅시다.

Subject는 Output과 Failure 타입을 지정해서 생성자를 호출하여 생성할 수 있습니다.

PassthroughSubject는 Passthrough Subject를 생성하는 생성자입니다.

 

Subject는 Publisher를 구독할 수도 있고,

sink 오퍼레이터를 통해 Publisher처럼 Subscriber를 등록할 수도 있습니다.

 

이렇게 send를 이용해 값 전송도 할 수 있고,

share 오퍼레이터를 통해 Passthrough subject를 Stream에 주입할 수도 있습니다.

 

이처럼 Subject는 굉장히 강력하고 유용합니다.

 

Working with SwiftUI

마지막으로 SwiftUI와 통합하여 사용하는 Subject를 알아봅시다.

 

SwiftUI는 개발자가 앱에 의존성을 설명해주면, 나머지는 프레임워크에서 모두 관리해 줍니다.

Combine의 관점에서 이는 데이터가 언제, 어떻게 변경되는지를 정의한 Publisher만 제공해주면 된다는 것을 의미합니다.

 

이를 위해 BindableObject 프로토콜을 채택하기만 하면 됩니다.

BindableObject는 PublisherType이라는 associatedType을 하나 가지고 있습니다.

이 타입은 Publisher의 Failure 타입이 Never여야 합니다.

이를 통해 Publisher에 도착하기 전에 Upstream 에러를 모두 처리하도록 강제하게 됩니다.

 

didChange 프로퍼티는 실제 Publisher에게 해당 프로토콜을 채택한 타입의 값이 바뀌었다는 것을 알려줍니다.

 

한 번 실제로 사용해 봅시다.

WizardModel은 BindableObject 프로토콜을 채택했고,

didChange 프로퍼티를 정의했습니다.

BindableObject을 채택했으므로 Subject를 통해 특정 종류의 값을 전송할 필요가 없고,

SwiftUI 프레임워크가 메서드 호출 단계에서 어떤 값을 전송해야할지 결정합니다.

위 코드에서 우리는 Void 타입으로 Output을 설정했습니다.

 

값이 변경될 때마다 알기 위해

trick와 wand에 didSet 설정을 해서 값이 변경될 때마다 Subject에 값을 전송하도록 합니다.

 

이제 SwiftUI는 WizardModel의 값이 변경되면 자동으로 Text를 변경합니다.

 

Designed for composition

Combine에는 Publisher, Subscriber, Subject를 포함해 90개가 넘는 오퍼레이터를 지원합니다.

 

이를 이용해 만드려고 했던 샘플 앱을 완성해보죠!

사용자 이름이 유효한지 서버를 통해 확인하고,

비밀번호 필드와 비밀번호 확인 필드가 동일한지 확인하고,

비밀번호가 8자 이상인지 확인해야 합니다.

 

또, 이 조건들을 확인해서 Create Account 버튼이 활성화, 비활성화를 결정해야 합니다.

 

이 작업들을 Combine을 이용해 동기적, 비동기적 작업을 모두 통합해서 처리해보겠습니다.

 

먼저 Combine을 이용하지 않았을 때의 모습입니다.

비밀번호 TextField와 비밀번호 확인 TextField는 값이 변경되면 이벤트가 발생하도록 target action을 추가할 수 있습니다.

 

이 작업을 다른 동기 처리 동작과 함께 동작하도록 해야하는데요.

이때는 Published를 각 프로퍼티에 추가해주면 됩니다.

 

Published

Published는 Swift 5.1에 추가된 Property Wrapper입니다.

주어진 프로퍼티에 Publisher를 추가해주는 역할입니다.

 

password에 Published를 붙여 Publisher를 추가했습니다.

Published가 붙은 프로퍼티는 이름 앞에 $ 를 붙여 wrap된 값에 접근할 수 있습니다.

그러면 위 코드처럼 Publisher나 Subscriber에서 사용할 수 있는 모든 Operator를 사용할 수 있습니다.

이 예제에서는 sink를 사용하여 값이 변경될 때 subscriber는 변경된 값을 전달 받습니다.

 

우리는 두 개의 Published 프로퍼티를 만들었습니다.

두 Publisher 모두 String을 Output 타입으로 가지고, Failure 타입으로 Never를 가집니다.

 

이제 이 두 개를 비교해서 비밀번호가 유효한지 판단해야 합니다.

즉, 두 개의 Output을 하나의 Output으로 변환해야 하죠.

위에서 알아봤듯이 CombineLatest를 이용하면 유용합니다.

 

validatedPassword는 AnyPublisher 타입의 계산 프로퍼티입니다.

Output 타입으로 String? 을 가지고, Failure 타입으로 Never를 갑니다.

 

CombineLatest를 이용해 둘 중 하나라도 변경된다면 이벤트가 발생하도록 했습니다.

password와 passwordAgain이 같은지 판단하고, 8자 이상인지 확인한 후

모두 만족하면 password를 반환하고, 아니라면 nil을 반환합니다.

 

"특정 비밀번호는 못 쓰게 해주세요."라는 추가 요구사항이 있어서,

map을 이용해 우리가 지정한 패스워드는 사용하지 못하게 했습니다.

 

마지막으로 eraseToAnyPublisher 오퍼레이터를 이용해 

최종 Output과 Failure 타입은 유지하되, 구체적인 중간 과정을 숨길 수 있습니다.

 

이는 최종 결과가 Map<CombineLatest<published<String>, published<String>, String?>> 같이 중간 과정이 포함된 복잡한 타입이 아니라,

AnyPublisher<String?, Never>로 반환하기 위함입니다.

 

Debounce

이제 사용자 이름을 서버를 통해 유효성 검사를 하는 비동기 처리를 해봅시다.

 

비밀번호와 마찬가지로 username 프로퍼티에 Published를 붙여 Publisher를 추가합니다.

 

하지만 비밀번호와 다른 점은 변경이 될 때마다 이벤트를 발생시키지 않고,

입력이 멈췄을 때 서버 통신을 했으면 좋겠습니다.

 

이를 위해서 debounce를 지원합니다.

debounce는 특정 윈도우를 만들고, 윈도우보다 더 빠르게는 값을 받지 않도록 할 수 있습니다.

Upstream Publisher와 Subscriber 사이에 Debounce를 넣어서,

사용자가 타이핑을 할 때마다 Debounce에게 값이 전달되지만,

Debounce는 자신의 윈도우에 맞게 속도를 조절해서 Subscriber에게 값을 전달합니다.

 

removeDuplicates

한 단계 더 생각해보면,

같은 값을 입력했을 때 굳이 서버 통신을 하지 않아도 됩니다.

이럴 때는 removeDuplicates를 이용하면 좋습니다.

removeDuplicates 오퍼레이터를 사용하면 같은 debounce 윈도우 내에서

한 번 발행된 값은 다시 발행되지 않도록 할 수 있습니다.

 

이를 종합해서 코드로 표현하면

이렇게 되겠네요 ㅎㅎ

0.5초 동안의 윈도우를 만들고,

이 시간동안 도착하는 값 중 중복을 제거하여 서버에 요청을 보냅니다.

 

이 코드는 서버 통신 처리를 효율적으로 해주었지만,

아직 비동기 처리는 하지 않았죠

 

Future

이제 비동기 처리를 해보겠습니다.

이미 작성한 usernameAvailable 메서드가 서버 작업이 완료되면 결과를 발생하도록 하고 싶습니다.

 

이는 flatMap을 이용하면 되겠지만,

안에서 어떻게 함수를 호출하면 될까요??

 

이때는 Future라는 Publisher를 사용하면 됩니다.

flatMap 안에서 Future를 사용해 promise를 받는 클로저를 정의합니다.

promise는 작업의 성공 혹은 실패를 결과로 받는 (Result<Output, Failure>) -> Void 타입의 클로저입니다.

 

이제 usernameAvailable을 호출하면

이렇게 코드로 표현할 수 있습니다.

 

이 코드의 흐름을 그림으로 표현하면 이렇습니다.

username이 변경되면 debounce를 통해 0.5초마다 전달되는데, Subscriber에게는 중복된 값이 제거되어 전달됩니다.

 

flatMap에서 Upstream으로부터 받은 값을 Future에서 userNameAvailable에게 전달한 다음,

클로저의 결과로 promise를 받아서 Future를 설정하고, flatMap이 Future를 Downstream으로 내려줬습니다.

 

마지막으로 eraseToAnyPublisher로 최종 결과 타입을 다듬어서 Subscriber에게 전달하는 흐름입니다.

 

그렇게 해서 최종적으로

이렇게 두 개의 Publisher가 탄생했네요.

 

 

이제 마지막 요구사항만 남았습니다.

 

일단 Valid 한지는 CombineLatest를 이용해 해결이 가능합니다.

이건 여러 번 했으니 이제는 익숙하실 거에요.

 

마지막으로 UI를 변경해주면 됩니다.

값이 발행되면 nil을 제거하고, UI를 변경하기 위해 메인 스레드로 흐름을 옮긴 뒤,

signupButton의 isEnabled 프로퍼티에 값을 할당합니다.

 

전체 흐름

샘플 앱의 전체 흐름을 보면,

이런 그림이 되네요 ㅎㅎ...

 

감사합니다!


아직은 초보 개발자입니다.

더 효율적인 코드 훈수 환영합니다!

공감 댓글 부탁드립니다.

 

 

 

반응형