안녕하세요 Niro 입니다.
WWDC 19 영상을 통해 Combine 에 대해서 알아봤으니 사용을 해보아야겠죠?
Combine 을 모르신다면
글을 읽고 오면 충분히 이해할 수 있을거라 생각합니다!
우리는 앞서서 Publisher 와 Subscriber, Operator 를 살펴보았습니다.
Subscriber 는 Publisher 를 구독하게 되고 해당 Publisher 로 부터 값을 downstream 으로 이동하기 시작합니다.
해당 과정은 Completion 되었거나 Failure 로 인해 Publisher 가 값을 보내는 것을 중지하거나 누군가가 구독을 취소하기전까지 계속 진행됩니다.
위의 내용처럼 Combine 의 핵심은 바로 시간에 따라 값을 처리할 수 있는 API를 설명하는 통합 선언적 API 라는 것입니다.
영상에 나온 예시는 마법 학교를 개설하려는 앱을 개발한다고 합니다.
앱의 기능에는 어떠한 마술 트릭을 다운로드 할 수 있도록하는 것인데 Combine 을 통해 구현해보고자 합니다.
Combine 을 통해 NotificationCenter 가 Publisher 와 함께 알림을 노출 시킬 수 있는 tricknamePublisher 를 만들었습니다.
여기서 중요한 것은 Combine 에서 Publisher 는 Output 과 Failure 타입이고 Notification 과 Never 라는 것을 잊으면 안됩니다.
Combine 에서 제공하는 map
함수를 이용해서 Notification 을 원하는 형식인 Data
로 변환할 수 있게 됩니다.
Sequence 에서 존재하는 map
과 매우 유사하고 여전히 에러를 발생시킬 수 없는 Never 라는 것도 알 수 있습니다.
자, 이제 우리는 Operator 를 통해 새로운 Publisher 반환하는 함수를 호출할 겁니다!
JSON 형태로 전달받은 순수한 데이터를 디코딩 하기 위해 우리는 tryMap
이라는 Operator 를 사용합니다.
해당 데이터를 MagicTrick 타입으로 변환하게 되고 map 과 유사한 작업처럼 보이지만 stream 에서 발생하는 오류를 Failure 로 변환하는 기능이 추가되었습니다.
결과적으로 Failure 타입은 Never 에서 Error 로 변환된 것도 볼 수 있습니다.
놀랍게도.. 데이터를 디코딩하는 작업이 매우 흔한 작업이라 그런가 이를 처리하는 Operator 도 따로 존재합니다.
tryMap 이 아닌 decode 를 호출하면 Publisher 의 Output 과 Failure 의 유형이 바뀌지 않으면서 위와 같이 간단해집니다!
우리는 지금까지 절대 실패할 수 없는 Publisher 를 보았는데 실패를 할 수 있는 Publisher 도 있기 때문에 Error Handling 을 하는 것이 중요합니다!
심지어 Combine 에서는 잠재적인 Fail 에 대해 적절하게 반응하는 것이 더욱 중요하다고 합니다.
그렇기 때문에 Publisher 와 Subscriber 가 Failure 에 대해 정확한 타입을 설명하는 작업이 필요한거 같습니다!
Combine 에는 Fail 에 대해 대응하고 복구할 수있는 많은 Operator 를 제공하는데 가장 간단한 것 중 하나는 assertNoFailure
를 사용하는 것입니다.
결과적으로 Publisher 의 Failure 는 Never 로 바뀌게 되는데 그 이유를 살펴 보겠습니다.
Upstream 과 downstream 이 assertNoFailure
로 연결되어 있는 것을 볼 수 있습니다.
해당 Operator 는 값을 받게되면 downstream 에 그대로 전달하기 때문에 문제가 없지만 upstream 로 부터 Error 를 받게되면 프로그램은 멈추게 되고 다음과 같이 오류가 발생하게 됩니다...
다행히도 Combine 에서는 Failure 를 처리하기 위한 Operator 가 준비되어 있으며 upstream Publisher 와 다시 연결을 시도하거나 Failure 를 다른 유형으로 변환할 수 있도록 허용도 해줍니다.
특히 여기서 유용한 Operator 는 catch
로 error 가 발생한 경우 대비한 Publisher 를 제공할 수 있습니다.
이전과 비슷한 이미지이지만 assertNoFailure 대신에 catch Operator 를 사용한 것을 볼 수 있습니다.
기존에는 error 가 발생하면 upstream 과의 연결이 종료되지만
catch 를 통해 제공된 recovery 클로저를 호출하여 기존의 Publisher 를 새로운 Recovery Publisher 로 대체함으로써 error 를 복구할 수 있게 됩니다.
새로운 Publisher 가 생겼으니 다시 구독을 해야겠죠?
구독을 한 이후에는 자유롭게 값을 받을 수 있게 됩니다.
자, 이제는 이미지가 아닌 코드로 사용해보겠습니다.
assertNoFailure 를 사용한 코드와 크게 다른 부분은 없지만 새로운 Publisher 를 반환하는 것이 특징입니다.
Just
를 사용하여 publish 하려는 값을 가지고 있을 경우 특별한 Publisher 를 정의하게 되는데 단순히 값만 publish 하라는 의미입니다!
catch
를 통해 Recovery Publisher 의 Failure 타입은 Never 가 됩니다.
앞서 우리는 Notification Publisher 를 통해 디코딩 하고싶은 데이터를 받았고 decode Operator 를 통해 다른 타입으로 변환하였습니다.
디코딩 과정에서 다양한 이유로 Error 가 발생한다면 upstream 과 연결이 끊어지기 때문에 우리는 catch 를 사용해 Recovery Publisher 와 연결을 하여 해결하였습니다.
Error 를 해결하는 것은 좋았지만 기존의 upstream 과의 연결은 끊겼기 때문에 Notification 을 더 이상 받지 못하게 됩니다.
여기서 우리가 원하는 것은 Error 가 발생하더라도 기존의 upstream 과 연결을 유지하며 placeholder 를 사용하는 능력입니다.
다행히도 Combine 은 준비가 되어있고 flatMap
이라 부릅니다!
flatMap 은 map 과 매우 유사하게 동작합니다.
위의 이미지 처럼 upstream Publisher 에서 값을 받으면 새로운 Publisher 를 생성하게 됩니다.
또한 flatMap 은 중첩된 Publisher 에 대해 구독을 하고 그 결과값을 downstream 으로 제공하게 됩니다.
자, 어떻게 동작하는지 살펴보겠습니다.
이전과 똑같이 upstream 에서 flatMap Operator 로 값이 전달됩니다.
그 다음 flatMap 은 새로운 Publisher 로 변환하기 위해 클로저를 호출하게 되고 우리의 경우, 새로운 Publisher 는 Just, decode, catch 로 이어집니다.
마지막으로 flatMap 은 새로운 Publisher 를 구독하고 결과값을 downstream 에게 제공합니다.
만약 decdoe 에서 error 가 발생한다면 어떻게 될까요?
error 가 catch 에 도달하게 되고 Recovery Publisher 로 대체됩니다.
그리고 해당 Publisher 는 flatMap 에 의해 반환됩니다.
즉, 해당 작업은 절대로 실패하지 않음을 보장 할 수 있는 것 입니다.
역시 코드로 봐야겠죠?
flatMap Operator 를 도입한 것이 보이고 클로저를 통해 새로운 Publisher 가 반환됩니다.
catch Operator 는 마찬가지로 새로운 Publisher 를 만들어냅니다.
우리는 upstream 이 failure 를 처리했으므로 원래 하려고 했던 이름을 publish 하려고합니다.
publisher(for: ) operator 를 사용하고 type safe keyPath 를 통해 MagicTrick 의 데이터를 추출하는 새로운 Publisher 를 생성할 수 있게 됩니다.
예약 연산자라 불리는 Scheduled Operator 를 사용하여 실제로 일정을 잡는 것처럼, 특정 이벤트가 전달되는 시간과 위치를 설명하는데 도움을 줍니다.
이러한 Operator 는 RunLoop 나 DispatchQueue 에서 네이티브로 지원되며 우리는 downstream 수신 이벤트가 특정 스레드 또는 Queue 에서 전달되도록 보장하는 recevie(on: )
Operator 를 사용해보겠습니다.
해당 Operator 는 Output 과 Failure 타입을 변경하지 않음을 확인할 수 있었고 다른 Scheduled Operators 에게도 동일합니다.
이제 Publisher 의 나머지 부분을 보겠습니다.
flatMap
을 통해 새로운 Publisher 와 연결을 하고 pulisher(for: )
연산자를 통해 MagicTrick 내부로 들어가 MagicTrick name 을 추출하였고 recevie(on: )
연산자를 통해 작업을 메인 스레드로 이동시켰습니다
우리는 Publisher 와 Operator 를 통하여 많은 작업을 수행할 수 있었고 값을 시간에 따라 생성하는 새로운 조합을 제공하였습니다.
Publisher 는 Just 와 같이 동기적으로 값을 생성할 수도 있고 NotificationCenter 와 같이 비동기적으로도 값을 생성할 수 있습니다.
자, 우리는 여기서 pulished 한 값의 다른 측면에 초점을 맞춰야 합니다. 바로 값을 받는 측면입니다.
Publisher 와 마찬가지로 Subscirber 에는 Input 과 Failure 두가지 유형이 존재합니다.
또한 subscription
, value
, completion
을 수신하는 3가지 이벤트 함수가 존재하며 해당 함수들은 호출되는 순서가 잘 정의되어 있고 다음 3가지 규칙을 따른다고 합니다.
Failure
가 발생했음을 나타낼 수있습니다. 중요한 점은 한번 completion 이 되었다면 더 이상의 값을 보낼 수 없게 됩니다.
이 세 가지 규칙을 다음과 같이 요약할 수 있습니다.
Subscriber 는 단일 구독(subscription)을 받은 다음 하나 이상의 값을 받으며, 이후에는 게시가 완료되거나 실패한 것을 나타내는 단일 완료(completion)가 있을 수 있습니다.
여기서 "가능성" 이라는 단어를 사용한 이유는 완료가 선택 사항인 경우가 많기 때문입니다.
NotificationCenter 의 예에서 보았듯이 많은 특정 스트림은 잠재적으로 무한할 수 있습니다.
Combine 에서는 다양한 종류의 Subscriber 를 지원하고 어떻게 작동되는지 살펴보겠습니다.
Combine 의 가장 간단한 구독 중 하나인 assign(to: on:)
Operator 를 사용하여 Key Path 할당을 추가하였습니다.
upstream Publisher 에서 전달하는 모든 값이 지정된 객체의 지정된 key Path 에 할당 되도록 보장하게 됩니다.
또한 cancellation
연산자는 또한 구독을 종료할 수 있는 취소 토큰을 생성하며, 나중에 호출하여 구독을 종료할 수 있습니다.
Publisher 가 이벤트를 전달하기 전에 구독을 중단할 수 있는 것이 유리하기 때문에 cancellation
을 Combine 형태로 구축하였습니다.
해당 작업은 구독과 관련된 리소스를 해제하려는 경우 특히 유용합니다.
또한 취소할 수 있거나 취소될 수 있는 것을 설명하기 위해 Cancellable
protocol 이 있으며 더욱 편리한 AnyCancellable
도 존재합니다.
AnyCancellable
은 deinit 에서 자동으로 cancel 을 호출하는 기능을 제공하여 명시적으로 cancel 을 호출해야 하는 회수를 크게 줄일 수 있습니다.
sink
Operator 는 단순히 클로저를 제공하면 받은 모든 값에 대해 클로처가 호출되며 원하는 side effect 작업을 수행할 수 있게 됩니다.
assign
과 마찬가지로 sink
는 구독을 종료할 수 있는 취소 토큰을 반환하므로 구독을 종료하는데 사용할 수 있습니다.
Publisher 와 Subscriber 양쪽의 특징을 약간 가지고 있는 혼합된 Operator 입니다.
전달받은 값을 다수의 downstream Subscriber 에게 다시 보낼수도 있고 특히 중요한 점은 명령형으로 보낼 수 있게 해준다는 것입니다.
이것은 기존 코드 베이스와 함께 작업할 때 매우 중요합니다.
subject 를 사용하면 다수의 downstream Subscriber 에게 전송할 수있고 값을 명령형으로 보낼 수 있습니다.
Combine 에서는 두가지 종류의 Subjects 를 지원하게 됩니다.
Passthroughsubject
는 값을 저장하지 않고 Subject 를 구독한 후에만 값을 볼 수 있는 특징을 갖고 있습니다.
CurrentValuesubject
는 수신된 마지막 값의 기록을 유지하여 신규 Subscriber 도 값을 확인할 수 있습니다.
열심히 동작원리를 알았으니 코드를 봐야겠죠?
Subject 를 생성하는 것은 원하는 Output
및 Failure
유형을 지정한 다음 생성자를 호출하는 것만큼 간단합니다.
Subjects
는 upstream Publisher 에 구독할 수 있어 Subscriber 처럼 작동합니다.
sink
와 같은, 오늘 언급한 연산자 중 하나를 호출함으로써 Publisher 처럼 스스로 Subscriber 를 형성합니다.
send 를 사용하여 값도 명령형으로 보낼 수 있습니다.
실제로 subject
는 자주 사용하기 때문에 stream 에 주입하는 share
연산자를 통해 PassthroughSubject
를 주입하기도 합니다.
Subject 는 매우 강력하고 다양한 용도로 사용가능하니까 잘 알아두면 좋을거 같습니다!
네번째 마지막 종류의 Subscriber 에 대해 이야기 하고자 합니다.
SwiftUI 의 놀라운 점은 어플리케이션의 종속성만 설명하면 나머지는 프레임 워크가 처리한다는 것이빈다.
Combine 의 관점에서 데이터가 언제 변경되었는지를 설명하는 Publisher 를 제공해야 한다는 것을 의미합니다.
이를 위해서 우리는 BindableObject
protocol 을 준수하면 된다.
현재는 ObservableObject 로 이름이 바뀌었기 때문에 ObservableObject 로 설명하겠습니다..
ObservableObject 는 하나의 associatedtype
이 있고 Failure
가 Never
로 정해진 Publisher 이기 때문입니다.
Publisher 에 도달하기 전에 upstream 오류를 처리하도록 강제하기 때문에 UI 프레임워크에서 함께 작업할때 훌륭하다고 합니다!
마지막으로 didChange
라는 하나의 속성을 지정하면 이 속성은 형식이 변경될 때 알리는 실제 Publisher 를 생성합니다.
이처럼 ObservableObject
을 채택하여 모델 객체가 변경되었을 때를 설명하는데 subject
를 사용할 것입니다.
그리고 프레임워크가 body 메서드에서 호출하는 것에 의해 파악하기 때문에 subject
이 특정 유형의 값을 전송하지 않아도 됩니다. 따라서 subject
의 Output
유형으로 void
로 선택합니다.
이렇게 subject
를 사용하면 객체가 변경될 때마다 언제든지 메시지를 명령형으로 보낼 수 있으므로 매우 유연하게 사용할 수 있습니다.
그러나 지금은 간단한 속성 관찰자(property observer)를 사용하고 subject
에서 바로 send
를 호출하여 두 속성 중 하나가 변경될 때마다 모델 객체가 변경되었음을 나타냅니다.
다음으로 Model 을 SwiftUI 에 연결해야하기 때문에 Modle 을 @ObjectBinding 으로 선언하여 자동으로 Publisehr 를 찾고 구독할 수 있도록 만들어 주고 body 내부에서 Model 의 속성을 참조 하게 됩니다.
즉, SwiftUI 는 Model 변경되었음을 전달받는 경우마다 자동으로 새로운 body 를 생성하게 되는 것입니다.
지금은 @ObjectBinding 가 아닌 @ObservedObject 로 변경되었습니다.
Combine 에 대해 연습을 진행해보았습니다....
Combine 에는 본문에서 설명한 많은 내장 기능이 있어서 이러한 기능을 조합하여 강력한 기능을 만들 수 있습니다. 이 새로운 프레임워크로 가능한 비동기 데이터 흐름의 단순화에 대한 기대감이 큽니다.
Error 를 처리하는 방식과 다양한 Subscriber, Operator 를 다뤄볼 수 있었고 SwiftUI 와 Combine 이 왜 궁합이 잘 맞는지 알 수도 있었습니다.
영상을 정확하게 전달하고자 글이 굉장히 길어졌는데...
읽어주셔서 너무 감사드리지만 아직 내용이 더 남이 있답니다!
이러한 기능을 응용해서 더욱 효과적으로 통합하는 방법에 대한 글로 찾아올게요!