본문 바로가기

iOS/Combine Framework

[Swift] No1. Hi! Combine. 컴바인 전반적인 개념 파해치기

안녕하세요.

지금부터 컴바인을 소개 합니다! 배우면서 햇갈렸던 부분을 자세하게 풀어 나가려고 합니다.

  What is combine in swift?

WWDC 2019 Introducing Combine. Apple에서 소개한 Swift framework입니다. asynchronous reactive programming을 할수 있는 특징이 있습니다. Reactive programming으로 Apple's native, Apple's platform인 Combine과 thrid-party인 Rx가 있습니다. 경력이 좀 있는 RxSwift와 Combine 뭐를 배울지 고민 했지만 결국 둘 다 배워 보려고 합니다. Will Combine Kill RxSwift? 유명한 글이 있어서 우선순위로 1순위로 컴바인 먼저 deep dive하려고 다짐 했습니다. Combine은 iOS 13.0 이상부터 적용 가능합니다.

 

 Combine은 시간의 흐름에 따라 asynchronous하게 값을 처리합니다. operator를 통해 복잡하고 다양한 연산을 수행할 수 있습니다. 옵셔널 체이닝 할 때 처럼 operator를 chaining 해서 값을 처리할 수 있습니다. (각 operator의 Output은 asynchronous로 받지만 이전 operator의 결과에 따라 다음 operator가 실행됩니다.)  Asynchronous하게 데이터 처리할 때 NotificationCenter, Timer, Delegate, @escaping Closure, GCD 등이 존재했고 Combine이라는 기술이 새로 등장했습니다.

 

 네트워크 처리 또한 Combine으로 사용이 가능합니다. Modern concurrency로 등장한 WWDC 2021 async/await @escaping closure  callback을 쓰지 않고  await에 따른 suspend, resume을 통해 async한 로직이 종료되면 값을 반환할 수 있습니다. 자세한건 나중에 올릴 예정입니다. 이와 반면 Combine은 operator를 통해 시간의 흐름에 따라 처리할 수 있습니다. Completion handler로 @escapign colsuer를 사용해서 함수가 종료 됬는데도 언제 인자값의 클로저가 호출될지 모르는 상황을 자주 맞이하는 것과 다르게Combine, Async/await 둘 다 asynchronous를 지향함으로 코드 한줄 한줄 순서대로 시행됩니다. 

  Combine and UIControl?

 사용자의 상호작용에 따른 특정 액션이나 event를 전달하는 시각적 요소인 컨트롤의 기본 클래스입니다. UIButton, UITextField, UISlider 등 객체가 상속 받습니다. UIButton의 경우 addTarget(_:action:), UITextField의 valueChanged(_:)  등의 메소드를 통해 객체의 event를 observe할 수 있습니다. Rx의 경우 RxCocoa를 통해 UIControl을 상속한 객체의 바인딩을 쉽게 할 수 있지만 Combine의 경우 UIControl 이벤트에 대해 Combine operator로 Chaining이 불가능합니다(operator들 연속해서 사용 불가능 하다는 뜻). 물론 여러 해결책이 존재합니다 :) CombineCocoa라는 오픈 소스도 있는데 아직 써보진 않았습니다.(애플에서 추후 업데이트 해주면 좋을텐데...)

  Combine's concept

 Combine을 다루기 위한 핵심적인 key는 Publishers, Operators, Subscribers로 이루어져 있다는 것입니다. 처음에 publisher라는 단어와 subscriber라는 단어는 알면서도 정말 했갈렸습니다. 

  • Publisher : 값을 생산하고 호출하는 임무
  • Operator : 값을 변경하는 로직을 맡는 임무
  • Subscriber : 값을 받거나 돌보는 임무
 예를 들어

 A기업은 공장에 차를 5대 생산했습니다. 1년 후에 생산한 5대 모두 John이 산다고 합니다. 이때 차량을 구별할 수 있게 번호판을 다르게 찍고 부착하는 작업을 완료한 후에 John에게 전달했습니다.

 이 예시에서 Publisher는 A기업이고 5개의 값을 갖고 있습니다. 차를 구매하려는 사람이 없었지만 John이라는 사람이 구매하겠다고 직접 예약을 걸었습니다. Subscriber는 John입니다. A기업은 차를 전달해주면 되는데 이때 번호판을 다르게 하는 작업을 Operator를 통해서 한대 한대 작업한 후 번호판이 부착된 차량을 John에게 5대 모두 전달하고 판매 "전달 완료" 했다는 completion을 보냅니다. 이후 더 이상 차를 생산하지도 팔지도 않습니다.

 다른 상황으로 Subscriber 돈이 부족해서 천천히 사는데 꼭 차량을 전부 살거라고 합니다. 차량 다섯대를 전부 보내지 말고 2대 씩만 보내라고 Publisher에게 조건을 제시할 수 있습니다. 

 

  Publisher's concept

WWDC introcuding_combine.pdf

 시간의 흐름에 따라 Subscriber에게 값을 하나 또는 여러개 전송(publish)할 수 있습니다. Publisher는 두 개의 Generic Type을 갖고 있습니다.

  • Publiser.Output
  • Publisher.Failure

Output은 publish할 value를 의미합니다. 없거나 여러개의 값을 publish할 수 있습니다. 이때 값은 성공 또는 실패를 할 수 있고 실패를 할 경우 Failure로 Error를 thorw하게 됩니다.

 

 Publisher 내부 로직은 Operator chains를 통해 실행됩니다. 계산, 네트워킹, event 처리, error handling등 다양하게 할 수 있습니다. Publisher는 다양한 이벤트를 Subscriber에게 전송하는데 이때 대표적으로 3가지 종류가 있습니다. 

  1. value with Publiser's Output Generic Type
  2. 모든 값을 전송하고 완료한 successful completion
  3. Operator chain을 통해 Output을 바꿀 때 발생할 수있는 error 로 인한 Failure with Publisher's Failure Generic Type

 Publisher의 장점은 단순히 값을 받기 위하는 Subscriber(구독자)에게 값을 publish 할 뿐 아니라 Failure 타입 또한 publish할 수 있다는 장점이 있습니다.

 

 Publisher의 subscribe(_:)는 Publisher에게 subscriber를 부착할 수 있습니다. 이때 Publisher의 receive(subscriber:)를 호출합니다. 그리고 이 둘의 공통점은 S.Input == Output, S.Failure == Failure 타입. 즉, Publisher와 Subscriber의 generic 타입이 전부! 일치해야 합니다. 일치 한다면 subscription을 생성합니다. 그리고 Publisher와 Subscriber는 subscription을 통해서 연결됩니다. 

 

  Subscriber's concept

WWDC introcuding_combine.pdf

Publisher한테 값을 받으려면 generic type이 일치해야합니다. Subscriber를 커스텀으로 구현할 수 있는데 Publisher와 연결된 후에 Generic type을 동기화 시키기 위해 associatedtype을 선언한 것을 알 수 있습니다. Subscriber 또한 두 개의 Generic Type를 갖고 있습니다.

  • Subscriber.Input
  • Subscriber.Failure

Publisher가 값을 받고자 하는 Subscriber와 subscription을 통해 연결 되었다면 Publisher의 0~n개의 chains로 부착된 Operator들을 통해 값을 processing하고 subscriber에게 값을 전달합니다. 위에서 제공된 프로콜을 채택하는 custom Subscriber를 만들 경우 Demand를 통해 Publisher에게 값을 특정한 개수만 달라고 요청할 수도 있습니다.

 

한가지 중요한 점은 Combine에선 두 개의 내장된 Subscriber를 추가 제공합니다.

  • sink // with Closure
  • assign // with KVO key path

개인적으로 가장 많이 사용하는 Sink! WWDC에서 Combine 소개 영상을 볼 때 처음에 Publisher는 Subscriber에게 값을 전달해 준다고 했는데 예제에서 Publisher에 Subscriber가 부착된 코드가 없음에도 subscriber에게 값을 전달 했다는 표현을 했습니다. 그 이유는 Subscriber를 직접 부착하지 않고 "sink(_:)"를 사용했기 때문입니다. sink(_:) 함수는 클로저를 통해서 subscriber를 부착하는 형태입니다. (처음에 이를 몰라서 해맸습니다 ㅠㅠ) 때문에 위와 같은 형식의 Subscriber protocl을 구현하지 않고도 Publisher의 3가지 대표적인 이벤트!!에 따라 값을 받거나 succeesful completion을 받을 수 있습니다. 에러가 발생한다면 completion 클로저에서 받을 수 있습니다.

 

  Subscription's concept

 가장 많이 사용하는 Subject의 경우 Publisher의 operator chain의 끝에 subscriber와 연결된 부분이라고 생각합니다. Subscription의 타입인 cancellable을 통해 Publisher와의 연결을 해제할 수 있습니다. Subscriber를 커스텀으로 구현할 경우 Publisher에게 Subscriber.Demand에 따른 전송 제한을 요청할 수 있습니다. 위에서도 언급했지만 subscribe(_:) 와 receive(subscriber:)를 통해 subscription이 생성됬다는 것은 Publisher와 Subscriber가 연결되었고 Publisher는 subscriber에게 값을 emit을 할 수 있다는 의미가 됩니다. 반대로 subscription이 없다면, Subscriber가 Publisher에게 부착되지 않았다면 Publisher는 값을 전송할 수 없습니다.(위에서 자동차 예와 같이 기업에서 자동차를 팔고자 하는데 팔 수 없습니다. 판매 대상이 없기 때문죠)

 

틀린 부분이 발견시 알려주신다면 정말 감사합니다!!!!

 

참고

https://www.kodeco.com/books/combine-asynchronous-programming-with-swift

 

Combine: Asynchronous Programming with Swift

Master declarative asynchronous programming with Swift using the Combine framework! Writing asynchronous code can be challenging, with a variety of possible interfaces to represent, perform, and consume asynchronous work — delegates, notification center,

www.kodeco.com