본문 바로가기

iOS공통

[Swift] Combine 1편: Combine, publisher 그리고 subscriber

 

안녕하세요! 주니오스입니다.✋

 

 

오늘은 Combine프레임워에 대해 Apple공식문서를 읽고 제가 이해한 내용을 이글에 정리해 보려고합니다.

 

 

Combine프레임워크란?

먼저 Combine프레임워크란 무엇인지에 대해 살펴보겠습니다.

 

 

공식문서의 내용을 해석하자면 다음과 같습니다.

 

 

실행시간에 변경되는 일련의 값들을 publisher(이하 pub)를 통해 subscriber(이하 sub)들에 전달하는 API이다.

 

 

즉, 변경되는 값 C에대해 A라는 pub이 B, D, E..같은 sub들에게 해당 변경사항을 전달하는 것이라고 해석할 수 있습니다.

 

 

여기서 pub이 sub들에게 전달하는 변경사항은 공식문서에는 element라고 적혀있으니 인지하기 바랍니다.

 

 

 

Combine이거 왜쓰는데?

 

이쯤대면 이 프레임워크를 왜 사용하는 것인지 의문이 들 수 있습니다.

 

 

왜냐하면 아직 Combine의 장점을 설명하지 않았거든요.

 

 

Combine에는 Operator(이하 Opt)라는 개념이 있습니다.

 

 

Opt는 pub가 publish한 element를 조작할 수 있습니다.

 

 

예를들어 Collection의 요소에 map함수를 쓰는 것처럼 말이죠.

 

 

아래코드는 NotificationCenter타입으로 생성한 pub에 map이라는 Opt를 장착한 것입니다.

 

 

이렇게하면 pub가 publish하는 element는 map Opt를 거치게 됩니다.

let sub = NotificationCenter.default
    .publisher(for: NSControl.textDidChangeNotification, object: filterField)
    .map( { ($0.object as! NSTextField).stringValue } )

 

map Opt는 pub를 반환합니다.

 

 

이렇게 반환된 pub에 또 원하는 Opt를 부착할 수 있습니다.

 

 

이렇게 pub가 연속적으로 생성 되는 것을 pub체인⛓이라고 합니다. 

 

 

pub체인을 사요한 element가공은 Combine을 사용하는 강력한 장점들중 하나입니다.👆

 

 

 

pub - sub

 

 

pub와 sub관계 구현 방법을 알아보기 전에

 

 

두 객체가 어떻게 동작하는지 단계로 인지하는 것이 중요합니다.

 

 

  1. pub를 먼저 생성합니다.
  2. sub를 생성합니다.
  3. pub에 sub를 등록합니다(Connect).
  4. sub가 pub에게 element를 요구합니다(Demand).
    • 중요한 개념으로 다음글에서 자세하게 다루겠습니다.
  5. pub는 sub의 요구에 publish로 반드시 호흥합니다.
  6. sub가 연결을 해제하거나 pub가 publish작업이 끝났음을 sub에게 알립니다.
    • 👆publish작업이 끝났다는 것은 element를 publish했다는 것이 아니라 pub의 publish가 종료되었음(에러에 의해 혹은 의도에 의해)을 sub에게 알리는 것이다. 즉, 더이상 pub는 publish를 하지 않음을 말하는 것이다.
  7. 연결(Connection)을 종료합니다.

 

 

 

위 단계를 인지하며 아래내용을 읽어주시길 바랍니다.

 

 

pub는 Publisher프로토콜을 채택하는 타입의 인스턴스를 의미합니다.

 

Publisher프로토콜은 Input, Failure이라는 연관타입을 가집니다.

 

 

여기서 Input은 pub가 publish할 element의 타입을 의미합니다.

 

Failure은 publish과정에서 에러가 발생할 시 전달할 타입입니다.

 

 

sub는 Subscriber프로토콜을 채택하는 타입의 인스턴스를 의미합니다.

 

sub역시 연관타입을 가지는데 Output, Failure타입을 가집니다. 

 

감이오시나요?🤔

 

 

sub와 pub가 서로 연결되려면 OuputInput타입이 같아야 하며 Failure타입도 서로 같아야 합니다.

 

 

하지만 이렇게 두타입의 연관타입을 일치시키는 것은 다소 까다로운 일입니다. 

 

 

그래서 Combine프레임워크는 pub의 Output, Failure타입과 연결가능한 sub를 자동생성하는 매서드를 제공합니다.

 

 

바로 sinkassign입니다.

 

 

 

먼저 sink부터 살펴봅시다.

 

sink(receiveCompletion:receiveValue:)

 

 

 

sink는 클로저 2개를 매개변수로 전달받습니다.

 

 

 

첫번째 클로저인 receiveCompletion은 앞의 6번단계에세 pub에게 publish작업이 끝났음을 전달 받을 때 호출되는 클로저 입니다.

 

 

receiveValue는 pub에 의해 element가 publish될 때 호출되는 클로저로 매개변수로 element를 받습니다.

let subscriber = NotificationCenter.default
    .publisher(for: NSControl.textDidChangeNotification, object: filterField)
    .sink(receiveCompletion: { print ($0) },
          receiveValue: { print ($0) })

 

 

assign은 키패스와 해당 키패스가 적용될 객체를 매개변수로 전달받습니다.

 

assign(to:on:)

 

 

 

키패스의 프로퍼티는 pub의 Output타입과 일치해야 합니다.

 

 

 

아래코드는 pub가 publish한 element를  myViewModel.filterStrin에 저장하는 코드입니다.

let sub = NotificationCenter.default
    .publisher(for: NSControl.textDidChangeNotification, object: filterField)
    .map( { ($0.object as! NSTextField).stringValue } )
    .assign(to: \MyViewModel.filterString, on: myViewModel)

위코드에는 주목할 점이 한가지 더 있습니다.

 

 

pub의 Output타입이 map Opt사용 전후로 다르다는 것입니다.

 


특정 타입에서 stringValue만 추출하여 이 것을 Output타입의 인스턴스로 지정한 것입니다. 아름답지 않나요?🤣

 

 

이것이 pub체인의 강력한 점중 하나입니다. 

 

 

pub를 따로 저장해두고 sub를 등록할 때마다 각기다른 Opt를 적용해 봅시다.

 

let pub = CurrentValueSubject<String, Never>("Hello")

let sub1 = pub.map { str in str + "!"✋ }.sink { print("sub1 accpeted \($0)") }

let sub2 = pub.map { str in str + "?"✋ }.sink { print("sub2 accpeted \($0)") }

pub.send("Data")

///sub1 accpeted Hello!
///sub2 accpeted Hello?
///sub2 accpeted Data?
///sub1 accpeted Data!

 

 

한번의 publish이지만 서로다른 Opt에의해 다른 element가 전달된 것을 확인할 수 있습니다.

 

 

Publisher프로토콜의 Apple공식 문서에 map과 같이 유용한 Opt들이 정말 많습니다.

 

https://developer.apple.com/documentation/combine/publisher

 

 

그런데 여기서 궁근한 것이 element는 Collection타입도 아닌데 왜 map, fileter같은 Opt를 사용하는 것이지?🤔 

의문이 생길 수 있습니다. 

 

 

앞서 Combine에 대한 내용을 다시보시면 element는 일련의 값들입니다.

 

비록 전달하는 값이 하나여도 element는 Sequence개념으로 이해해야 합니다.

 

 

이부분에서 AsyncSequence타입(프로토콜 입니다)사용과 Combine방식이 유사하여 비교되는 부분입니다.

 

이것은 추후에 다뤄보도록 하겠습니다.

 

 

sink와 assign 두 매서드를 사용해 생성한 sub는 pub에게 Demand수의 제한을 두지 않습니다. (unlimited demand)

 

이말은 지금까지는 sub가 pub에게 무한히 계속 요구를 한다라고 이해하면 됩니다. 

 

 

Connection끊기

 

 

sink와 assign으로 생성한 sub의 경우 cancel매서드를 호출하여 연결을 종료할 수 있습니다.

 

커스텀 Subscriber의 경우는 다음글에 다루도록 하겠습니다.

 

let sub1 = pub.map { str in str + "!" }.sink { print("sub1 accpeted \($0)") }

sub1.cancel()

 

 

이 글을읽고 Combine이 무엇이고 어떤 느낌인지 파악하셨을 꺼라고 생각합니다.

 

 

다음글에서 봐요!✋✋✋✋

 

 

 

 

Combine | Apple Developer Documentation

Customize handling of asynchronous events by combining event-processing operators.

developer.apple.com

 

 

Receiving and Handling Events with Combine | Apple Developer Documentation

Customize and receive events from asynchronous sources.

developer.apple.com