티스토리 뷰
WWDC 2019에서 Combine이 발표되었다.
Combine은 Rx와 똑같다! 라고 말하던데 정말 똑같을까..? 🤔
Rx와 Combine을 비교해보자
1. 스펙 비교
Rx와 Combine은 모두 Reactive 프로그래밍을 위한 framework이다
하지만 Rx는 iOS 8이상부터, Combine은 iOS 13이상부터 사용할 수 있다
(그래서 Combine을 실제 프로덕트에서 쓰기까지는 최소 1년에서 최대 3년까지 걸릴 것이라고 말한다)
Rx는 Third party framework인 반면 Combine은 애플에서 만든 buit-in framework이다...!
그리고 Rx는 Rxcocoa와 Combine은 SwiftUI와 UIBinding을 하도록 설계되어졌다
(하지만 Combine은 SwitUI와만 쓸수있고 UIKit과 못쓴다 이런 개념은 아니다! 뒤에 가서 설명하겠음)
2. 성능 비교
어떤 분이 0부터 천만까지 있는 sequence를 연산할때 Rx와 Combine의 성능이 어떤지 비교해보는 실험을 해주셨다.
결과는 Combine이 시간이나 메모리 할당면에서 모두 성능이 더 좋았다
이 실험 내용이 있는 아티클은 여기서 볼 수 있다
https://medium.com/flawless-app-stories/will-combine-kill-rxswift-64780a150d89
3. 기본 개념 비교
-
1. Observable과 Publisher
-
2. Subject
-
3. Change Thread
-
4. Disposable과 Cancellable
-
5. Operators
이렇게 다섯 가지 개념을 비교 해볼 것이다
3.1 Observable과 Publisher
Rx의 Observable = Combine의 Publisher
Rx의 Observer = Combine의 Subscriber
하지만
Publisher는 프로토콜이고 AnyPublisher는 Publisher을 따르는 struct이다
Subscriber는 프로토콜이고 AnySubscriber는 Subscriber를 따르는 struct이다
그러니까 Observable은 AnyPublisher와 같고 Observer는 AnySubscriber와 같다고 말하는 것이 정확하다
Observable과 Publisher의 차이가 있다면, Publisher는 만들 때 데이터 타입 뿐 만 아니라 에러 타입까지 지정해주어야한다는 것이다
이렇게 Observable은 데이터 타입만 지정해주면 된다
AnyPublisher<String, APIError>
AnyPublisher를 만들 때는 데이터 타입과 에러타입지정해주어야한다.
기본 Error 타입으로 지정해줄 수 있고 APIError 같은 커스텀 에러를 만들어서 지정해줄 수 도 있다
3.2 Subject
Rx와 Combine 모두 개념도 이름도 같은 Subject라는 녀석이 있다
Rx의 Subject
- Observable 과 Observer 모두로 동작한다
- 구독자들에게 value, error, completed 를 준다
- PublishSubject (initial value 없음) / BehaviorSubject (initial value있음)
Combine의 Subject
- Publisher와 Subscriber 모두로 동작한다
- 구독자들에게 value, error, finished를 준다
- PassthroughSubject (initial value 없음) / CurrentValueSubject (initial value있음)
완료이벤트를 Rx에서는 completed, Combine에서는 finished라고 부르는 것빼고는 정말 다 똑같다...!!!
initial value가 있는 Subject, 없는 Subject로 나눠놓은 것도 똑같다...!!
하지만 BehaviorSubject랑 CurrentValueSubject 는 조금 다른 점이 있다
BehaviorSubject는 subscriber가 구독한 시점에 초기값 또는 최신값(초기값 이후 발행된 값이 있다면)을 준다
CurrentValueSubject는 subscriber가 구독한 시점에 초기값을 준다
밑의 코드를 보면서 Rx의 Subject와 Combine의 Subject를 비교하여보자 :-)
1) RxSwift의 PublishSubject / Combine의 PassthroughSubject
두개 다 초기값이 없고
구독자들에게 value를 주고 completed를 주고 그 이후에 스트림이 끝나서 "Is Alive?" 가 출력되지 않음을 볼 수 있다
2) Rx의 BehaviorSubject / Combine의 CurrentValueSubject
BehaviorSubject는 Init Value라는 초기값이 있고 그 이후에 "Is anyone listening?" 을 방출했다 (이 값이 최신값..!)
구독자들이 구독한 시점에 받은 값을 보면 "Is anyone listening?" 을 받았음을 알 수 있다
BehaviorSubject는 구독자들이 구독한 시점에 초기값을 주거나 초기값 이후 발행된 값이 있으면 최신값을 주기 때문이다
하지만 똑같은 상황에서 CurrentValueSubject를 구독한 구독자들이 구독한 시점에 받은 값을 보면 "Is anyone listening?" 이 아닌 "Init Value" 이다
CurrentValueSubject는 구독자들이 구독한 시점에 초기값을 주기 때문이다 이 점이 BehaviorSubject 와 다르다
그 외에 구독자들이 value를 받는 것이나 completed 또는 finished 이후에 스트림이 종료되는 것은 두 Subject 모두 똑같다
3.3 Change Thread
멀티쓰레딩 관련해서도 비교해보자
Rx에서 연산이나 구독 같은 특정 작업의 쓰레드를 지정해줄 때 observeOn 을 쓴다
Combine에서도 이와 같은 용도인 receiveOn(on:) 이 있다
그리고 Rx에서 Observer가 동작할 쓰레드를 지정해줄 때 SubscribeOn을 쓴다
Combine에서도 이와 같은 용도인, 이름도 같은 Subscribe(on:) 이 있다
하지만 Rx의 SubscribeOn은 위치에 상관없이 쓸 수 있다면 Combine의 Subscribe(on:)은 위치에 상관있다는 차이점이 있다
이 차이점은 뒤에 가서 코드와 함께 살펴보기로 하고 우선 Rx와 Combine에서 Thread바꾸는 것을 어떤 식으로 쓰는 지 부터 살펴보자 : )
우선 Rx를 살펴보자
Observable이 동작할 쓰레드를 subscribeOn으로 지정해주었다
그리고 observeOn으로 특정 작업의 쓰레드를 지정해주었다 (observeOn works on downstream!)
(API에서 데이터가져오는 작업은 background에서 하시오 구독은 Main에서 해서 view의 visible여부 바꿔주는 작업을 하시오 하고 있는 것이다)
그 다음 Combine을 살펴보자
Publisher가 동작할 쓰레드를 subscribe(on:) 으로 지정해주었다
그리고 receiveOn으로 특정 작업의 쓰레드를 지정해주었다 (receiveOn works on downstream!)
역시 사용하는 방식도 똑같다...!
이제 차이점이 있다고 말했던 Rx의 SubscribeOn과 Combine의 Subsribe(on:) 을 샅샅이 살펴보자_!!
우선 Rx의 SubscribeOn에 대해서 명확히 다시 짚어보자
Rx의 SubscribeOn은 전체 체인에서 어디에 위치하든지 상관없이 Observable의 쓰레드를 바꿀 수 있는 녀석(?) 이다
이 그림을 보면 이해하기 쉬운데 subscribeOn이 밑에 있음에도 불구하고 Observable을 파란색으로 바꾸는 것을 볼 수 있다
그리고 observeOn이 downStream을 바꾸므로 주황색, 분홍색 으로 체인의 색깔이 바뀌는 것이다
실제 코드로 봐보자
Observable 바로 밑에 subscribeOn을 위치시켜주지 않아도 쓰레드가 잘 바뀌는 것을 볼 수 있다
즉 Rx에서는 SubscribeOn을 위치상관없이 사용해도 된다
그럼 Combine에서는 어떨까?
Rx와 똑같이 코드를 작성하였지만 예상대로 쓰레드가 바뀌지 않는 것을 볼 수 있다...!
이렇게 Publisher 바로 뒤에 Publisher가 동작할 스레드를 지정해주어야지 결과가 예상대로 나온다
즉 Combine에서는 SubscribeOn을 위치상관없이 사용하면 안된다
왜 두 개가 다른 것일까?!
애플 문서에 보면 "subscribe(on:) changes the execution context of upstream messages." 라고 나오는데
Rx의 subscribeOn은 upstream & downstream을 모두 바꾸기 때문이다
정리를 하면...!!
RxSwift
- ObserveOn = It works only downstream
- SubscribeOn = It works upstream & downstream
Combine
- receiveOn(on: ) = It works only downstream
- Subscribe(on:) = It works only upstream
3.4 Disposable과 Cancellable
Rx에는 Disposable과 DisposeBag이라는 개념이 있다
DisposeBag에 스트림을 담으면 deinit 될때 자동으로 dispose 된다
Combine에도 Disposable과 비슷한 개념인 AnyCancellable이 있다. deinit 될때 자동으로 cancel 된다
하지만 Rx의 disposebag 같은 개념은 없다
우선 AnyCancellable이 정말 deinit 될때 자동으로 cancel 되는지 부터 살펴보자 :-)
<간단한 실험>
5초 후에 이벤트를 발행하는 Publisher와 3초 후에 deinit되는 viewController가 있다
viewController가 deinit 되면 viewController 안에 있는 Publisher도 cancel 될 것인가?!
1) AnyCancellable에 넣어주지 않았을 때
deinit 되어도 stream이 계속 살아있다...!
2) AnyCancellable에 넣어줬을 때
deinit되면 구독의 관계도 같이 끊김을 볼 수 있다
AnyCancellable을 사용하면서 아쉬운 점이 두가지가 있었다
일단 DisposeBag과 대응하는 개념이 없다는 것....!
Disposebag이 없으니
var cancellables: [AnyCancellable] = []
bag과 비슷한 개념인 cancellables을 만들어서 스트림을 계속 여기에 담아주며 써야했다
결국 Combine은 CancelBag이 필요하지 않습니까????!!! 하는 아티클도 몇개 보았고
실제로 Combine의 disposeBag이라는 CancelBag 라이브러리도 나왔다
https://github.com/devxoul/CancelBag
이런 불편함은 두번째 문제이고 사실 가장 큰 문제는 실수할 위험이 있다는 것이다...!!
이게 무슨말이냐면...
밑의 코드는 내가 실제로 프로젝트하면서 AnyCancellable을 썼던 코드인데
(Combine의 assign은 Rx의 bind , Combine의 sink는 rx의 subscribe 와 비슷한 개념)
assign과 달리 sink를 한 stream은 AnyCancellable로 wrap해서 넣어주고 있음을 볼 수 있다
왜 그러냐면...!
public protocol Cancellable
final public class AnyCancellable : Cancellable
Cancellable은 프로토콜이고 AnyCancellable은 Cancellable을 따르는 클래스이다
assign은 AnyCancellable을 리턴하지만
sink는 Subscribers.Sink<Self> 를 리턴하는데
final public class Sink<Upstream> : Subscriber, Cancellable
이 Sink는 Cancellable 프로토콜을 따르고 있다
그래서 assign과 달리 sink는 AnyCancellable로 wrap해줘야하는 것이다...!
처음에 이것을 잘 모르고 썼을 때는 assign한 것도, sink한 것도 모두 Cancellable형 배열에 담기니까
Cancellable형 배열로 메모리관리를 해주었다
하지만!!! 이렇게 하면 안된다 AnyCancellable과 달리 Cancellable은 deinit될 때 auto-cancel이 안되기 때문이다...!!
애플 개발자 포럼을 보면 나같은 실수를 한 사람의 글을 볼 수 있다
demo 영상에서 말한 것과 다르게 Cancellable이 왜 deinit 될 때 자동으로 cancel 안되는 것인가요?! 라는 질문이였는데, 답변으로 sink한 결과를 AnyCancellable로 wrap하세요~ 라는 코멘트가 달렸다
앞으로 더 많은 사람들의 실수가 예상된다 😭😭😭
이와 달리 disposebag은 bind를 하든 subscribe를 하든 다 disposebag에 담겨서 좀 더 편하고 실수할 위험도 적은 것 같다 Disposebag 짱짱..
그 다음 또 아쉬운 점은 Combine은 cancel 여부를 확인할 블럭도 없다는 것이였다...
이렇게 Rx는 dispose여부를 확인할 수 있는 블럭이 예쁘게 있는데
Combine에는 없다 😭
----------------------------- 추가 ---------------------------
이런 메소드가 추가되었습니다!!
이제 이런 식으로 사용하면 됩니다--!!
밑의 사진은 렛츠스위프트에서 뱅.샐의 보영님이 Rx의 DisposeBag과 Combine의 Set<AnyCancellable> 를 비교해주신 슬라이드입니다
3.5 Operators
Rx와 Combine의 Operator들은 거의 비슷하다
예를 들어 Convenience Operator를 봐보자
이렇게 Just 연산자도 같고
Sequence 만드는 연산자도 같다
Combine Operator는 조금 다르다....!
Rx는 연산하는 observable 갯수와 상관없이 Operator가 하나 이다
하지만 Combine은 연산하는 publisher 갯수에 따라서 Operator가 여러개 이다
예를들어 Rx는 CombineLatest, Merge 이렇게 Operator가 있다면
Combine은 두개는 CombineLatest, 세 개는 CombineLatest3, 네 개는 CombineLatest4 이렇게 있고
두 개는 Merge, 세 개는 Merge3, 네 개는 Merge4 ...... 이런식으로 있다
그래서 실제 작업을 할 때 5개의 Publisher를 CombineLatest로 연산해야하는 상황이 있었는데,
combineLatest4 까지 있고 combineLatestMany는 없어서
4개를 먼저 연산하고 그 publisher를 다시 마지막 하나랑 연산하고... 이렇게 두번 연산해야하는 조금의 불편함이 있었다.. 😫
그리고 Combine에만 있는 try + operator 도 살펴보도록 하자
Combine은 Rx와 달리 에러를 던질 수 있는 Operator도 제공한다
Combine은 throwing operator와 not throwing operator를 나눠놓았다
모든 operator가 다 그런 것이 아니라 몇개의 operator 들만 try 붙은 (throwing) / try 안붙은 (not throwing) 버전이 있다
몇 가지 Operator들을 예시로 가져왔다 : )
그 중 map과 trymap을 한번 살펴보자
map은 이렇게 생긴 not throwing opertor 이다
public func map<T>(_ transform: (Output) -> T) -> Publishers.Just<T>
trymap은 이렇게 생긴 throwing opertor 이다
public func tryMap<T>(_ transform: (Output) throws -> T) -> Publishers.Once<T, Error>
에러 처리를 안에서 해서 반드시 결과를 리턴해줘야하는 map과 달리
trymap은 안에서 에러를 던질 수 있다. catch 로 체이닝을 하여 에러를 밖에서 처리해주었다
map에도 catch를 체이닝해보았는데 Error타입이 아니라 Never(에러없다) 타입을 받는다
catch { (never: Never) in
}
Operator 비교는 rxswift - to - combine cheatSheet 을 참고하시면 좋습니다 👍
'🍏 > SwiftUI + Combine' 카테고리의 다른 글
[SwiftUI] 뷰의 이니셜라이저에서 @State, @Binding, @EnvironmentObject에 접근하기 (1) | 2020.08.31 |
---|---|
[SwiftUI] Multi-platform App 만들기 - 프로젝트 세팅 (2) | 2020.08.06 |
[SwiftUI] ZStack 실전 예제들 (2) | 2020.05.16 |
[Combine] Cancellable과 AnyCancellable (6) | 2019.06.28 |
Rx에서 Combine으로 바꿔본 후기와 느낀점 (2) | 2019.06.28 |
- Total
- Today
- Yesterday
- 플러터 얼럿
- ribs
- 장고 Custom Management Command
- flutter 앱 출시
- Flutter getter setter
- flutter deep link
- cocoapod
- 플러터 싱글톤
- Flutter Spacer
- 구글 Geocoding API
- DRF APIException
- drf custom error
- 장고 URL querystring
- flutter build mode
- Python Type Hint
- Dart Factory
- Flutter 로딩
- PencilKit
- Django Firebase Cloud Messaging
- ipad multitasking
- SerializerMethodField
- Django Heroku Scheduler
- Flutter Text Gradient
- flutter dynamic link
- Watch App for iOS App vs Watch App
- github actions
- Sketch 누끼
- Django FCM
- METAL
- Flutter Clipboard
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |