안녕하세요 Niro 🚗 입니다!
그동안 WWDC 19 영상을 통해 Combine 의 특징과 Publisher, Subscribers 에 대해 알아보았습니다.
Combine 소개에 대한 마지막 편으로 Operator 에 대해 알아보고자 합니다.
예시가 쭉 이어서 진행되니 전편을 읽고 와주시면 더욱 이해가 빠르게 될거라 생각합니다!
내용이 많지만 정신 바짝 차리고 진행해보겠습니다!
Operator 는 값을 변경, 추가, 제거 또는 다양한 종류의 행동을 설명하는 것을 의미하고 upstream 이라 불리는 Publisher 를 구독하고 결과를 downstream 이라 불리는 Subscriber 에게 보내는 역할을 합니다.
Operator 는 특이하게 Publisher Protocol 을 채택 할 때까지 Publisher 라고 합니다.
채택을 하게되면 Operator 라는 거겠죠....? 이리저리 번역을 해봐도 잘 모르겠지만.. 일단!
이렇게만 보면 이해가 잘 안가니 예시를 볼까요?
Map
은 어떤 upstream 과 연결 되었는지, 해당 upstream 의 Output
을 클로저를 통해 Map
자체의 Output
으로 어떻게 변환하였는지를 초기화하는 구조체 입니다.
특이하게 자체적으로 Failure
를 생성하지 않고 upstream 의 Failure
를 그대로 전달한다고 합니다.
즉, Map
을 사용하게 되면 전편인 Subscriber 에서 오류가 났던 Notification 과 Int 간의 변환을 할 수 있게 된다는 것입니다.
오류가 났었던 코드로 돌아가겠습니다.
이전과 같은 Publisher 와 Subscriber 를 유지하고 graduationPublisher
에 연결되도록 구성된 converter
라는 상수가 추가된 것을 볼 수 있습니다.
converter
는 클로저로 구성되어 있고 graduationPublisher
에 의해 notification 이 전달 되었을 때, NewGrade
라는 사용자 정의 Key 에 대한 값을 찾게됩니다.
만약 값이 존재하고 Int 라면 클로저에서 해당 값을 반환하게 되고 만약 값이 존재하지 않거나 Int 가 아니라면 기본값으로 0 을 사용하게 됩니다.
즉, 어떤 경우에도 해당 클로저의 결과는 Int 이고 이것을 Subscriber 에 연결할 수 있게 된다는 것입니다!
움... 아까 봤던 Map 과 굉장히 다른 모습을 하고 있는 map
이라는 함수가 보이네요...
둘다 Publisher protocol 의 extension 으로, 모든 Publisher 에 사용 가능한 Operator 마다 일련의 함수를 추가한 것입니다.
Map 의 함수 중 하나인 것이죠!
해당 함수의 파라미터는 upstream 을 제외한 Map 을 초기화하는데 필요한 모든 것을 의미합니다.
왜냐하면 Publisehr 의 extension 으로 self
를 사용할 수 있게 되었기 때문입니다.
사실.. 저는 이게 어떤 효과를 가져오게 되는지 몸소 느끼지 못했지만.. 비동기 프로그래밍을 생각하는 방식을 변화시키는 중요한 것이라 합니다...
아시는 분 있으면 이유 좀 설명해주세요 ㅠㅠㅠㅠ
위에서 봤던 새로운 구문을 사용하여 예제로 돌아가봅시다!
여기서 Merlin 의 graduated
에 알림을 위한 NotificationCenter Publisher로 시작합니다.
Notification 을 받으면 동일한 클로저를 통해 매핑하게 되고 그 결과를 Merlin 의 grade
속성에 할당하게 됩니다.
위에서 봤던 구문과 달리 단계별로 무엇이 실행되는지 선형적이고 이해하기 쉬운 흐름으로 제공된다고 합니다.
그 아래는 assign
메서드가 보입니다.
Assign
메서드는 cancelable
이라고 불리는 것을 반환합니다.
Combine 에는 취소 (cancelation) 도 내장되어 있기 때문에 취소를 하게 되면 시퀀스를 조기에 해체할 수 있도록 합니다.
여기서의 각 단계별 구문이 Combine 을 사용하는 방법의 핵심입니다!
정리하자면 첫번째 Publisher 에서 시작하여 Operators 을 통해 Subscriber 에서 입력 받을 수 있는 방식으로 값을 변환하게 됩니다.
Combine 에서는 이러한 Operators 를 많이 갖고 있고 이것을 선언적 (Declearative) Operator API 라 부릅니다.
Map
과 같은 함수적 변환 뿐만 아니라 Publisher 의 첫번째, 두번째 또는 다섯번째 요소를 가져오는 것과 같은 List 작업인 Filter
와 Reduce
가 존재합니다.
또는 오류를 기본 값이나 다른 값으로 변환하는 것도 존재를 하고 Thread 나 무거운 처리 작업을 백그라운드 Thread 로 이동하거나 UI 작업을 위해 Main Thread 로 이동하는 Queue movemnet 도 포함이 됩니다.
또는 From loop, Dispatch Queue 와의 통합, 타이머, 타임아웃을 포함한 Schedling 및 Time 에 관련된 작업도 존재합니다.
참 종류가 많죠.....?
이처럼 사용할 수 있는 Operators 가 많기 때문에 어떠한 상황에서 어떤 것을 사용해야 할지 부담스러울 수 있습니다.
영상에서 권고하는 것은 Combine 의 핵심 디자인 원칙인 구성 ( Composition ) 으로 돌아가야 한다고 합니다...
일단 써보고 경험을 쌓는 것도 좋은 방법일거 같기도 하구..
자, 많은 일을 하는 몇가지 Operator 를 제공하는 대신에 아주 많은 Operator 를 제공하므로써 각각은 적은 일을 하게 되겠지만 이해하기 쉽게 만들어줄 수 있도록 구현되었다고 합니다.
개발자를 위한 Apple 의 배려라고 할까나..?
그래서 모든 Operators 를 탐색하는데 있어 도움을 주기 위해 Swift Collection API 에서 이름에 대한 영감을 얻었다고 합니다.
왼쪽에는 동기적인 API 가 있고 오른쪽에는 비동기적인 API 가 존재하는 사분면을 들고 왔습니다.
상단에는 단일 값, 하단에는 여러의 값이 있는 것도 보이네요!
Swift 에서 Int 를 동기적으로 표현해야 한다면 Int 와 같은 자료형을 사용합니다.
여러 Int 형을 동기적으로 나타내기 위해 Int Array 를 사용하게 되구요!
Combine 에서는 이러한 개념을 비동기적인 세계로 변환시켰습니다!
비동기적으로 단일 값을 나타내려면 나중에 발생하게 되므로 Future 가 있고
비동기적으로 여러 값을 나타내기 위해서 Publisher 가 존재합니다.
움.. 이미 배열로 수행하는 특정 유형의 작업이 있다면 이름을 Publisher 로 적용해보는 것이 좋겠네요!
자, 돌아와서 예시를 보여드리겠습니다!
여기서 Key 가 없거나 Int 가 아닌 경우 기본값으로 0 을 사용하도록 했었습니다.
그런데 말입니다..
부정확한 값을 사용하지 않고 Model Object 에 기록되지 않게 하는 것이 더 나은 방법이지 않을까요?
이 방법을 현실화 시킬 수 있는 한가지 방법은 해당 클로저가 nil 을 반환하도록 허용하고 nil 값을 필터링을 하면 됩니다!
Swift 4.1 에서는 이 작업에 대한 이름을 표준 라이브러리에서 소개를 했었습니다.
우리는 그것을 compactMap
이라 부릅니다!
당연히 Publisher 에도 compactMap
이 존재하고 매우 유사한 방식으로 동작합니다.
해당 클로저에서 nil 을 반환하게 되면 compactMap
은 그것을 필터링 하여 stream 을 더이상 진행시키지 않게 합니다.
자,몇가지 더 익숙한 이름을 이용하여 단계별로 작성해 보겠습니다.
학교에는 5학년 이상의 학생만 허용된다고 가정해 봅시다.
그러면 우리는 자연스럽게 Filter
가 생각이 나고 당연하게도 해당 작업을 수행할 수 있습니다.
Filter
는 조건자를 사용하게 되고 해당 조건을 통과하는 요소만 진행하도록 허용합니다. 이것은 Array 에서의 Filter
와 완전히 동일한 동작이죠??
한발자국 더 나아가 봅시다!
학년에 이어서 졸업은 최대 3번까지 가능하다고 가정해봅시다.
Array 에서 처음 3개의 요소를 가져오려면 .prefix(3)
를 사용하게 되는데 Publisher 에서도 처음 3개의 요소만 받기 위해 동일한 문법을 따르게 됩니다. 참 쉽죠~?
결과적으로 .prefix(3)
는 3개의 값을 받은 후 upstream 을 취소하고 downstream 에게 완료를 보내게 됩니다.
자 정리를 해봅시다.
Merlin 의 졸업에 대한 NotificationCenter Publisher 가 있었고 졸업하게 되면 해당 속성에서 newGrade 를 가져오게 됩니다.
해당 값이 5 보다 크고 Merlin 의 grade 속성에 할당하기 전에 최대 3번만 일어났는지 확인하게 됩니다.
Map
과 Filter
는 훌륭한 API 이지만 주로 동기적인 동작을 위한 것입니다.
Combine 은 비동기적인 환경에서 빛을 발하는데 어떤 Operator 를 사용하면 좋을까요..?
비동기 작업에서 빛을 발하는 두가지 Operator 를 소개하겠습니다.
첫번째로 Zip
에 대해 알아볼까요?
앱에서 사용자가 계속 사용하기 위해선, 그들의 Wand 가 생성될때까지 기다려야 합니다. 화면을 보게되면 오래 실행되는 비동기 작업 3개가 보이네요!
또한 하단에 3개의 비동기 작업이 완료 되면 활성화 되는 Continue 버튼이 존재합니다.
이렇게 여러개의 비동기 작업이 있을 경우 우리는 Zip
을 이용해야 합니다!
Zip
은 여러 upstream 의 Input 을 한개의 튜플로 바꿔줍니다. 계속 진행하기 위해선 모든 upstream 의 Input 이 필요하기 때문에
"이거, 저거, 그거 가 완료 되었을 때 다른 작업을 수행" 하는 것과 같은 "when / and" 작업으로 만들게 됩니다.
예를 들어, 첫번째 Publisher 는 A 를 생성하고, 두번째 Publisher 가 1를 생성할 때 튜플을 만들어내고 그 값을 Subscriber 에 내보내게 됩니다.
위와 같이 Boolean 결과를 반환하는 3개의 비동기 작업의 결과를 기다리기 위해 3개의 upstream 을 사용하는 Zip
을 사용하게 됩니다.
튜플을 하나의 Boolean 으로 매핑하고 여기에서 버튼의 isEnabled
속성에 할당하여 버튼을 활성화시키게 됩니다.
이번엔 어떤 약관에 대한 조건을 동의해야만 다음으로 넘어가는 화면이 보입니다.
Play 버튼이 활성화 되기전에 3개의 스위치가 모두 활성화 되야 하는 작업으로 보이는데...
당연히 3개중 하나라도 충족하지 않는다면 버튼을 비활성화 해야겠죠?
이러한 작업을 수행하기 위해선 바로 바로 Combine Latest
를 사용해야 합니다!
Zip 과 마찬가지로, Combine Latest
는 여러 upstream 의 Input 을 하나의 값으로 바꿔줍니다.
하지만 어떤 upstream 에서든 Input 이 필요하며, 이것은 "when / or" 작업의 일종입니다.
이를 지원하기 위해 각 upstream 에서 받은 마지막 값을 저장하고, 단일 downstream 으로 변환할 수 있는 클로저로 구성되어 있습니다.
예를 들어, 첫번째 Publisher 가 A 를 보내고 두번째 Publisher 가 1을 보내면 이것을 문자열로 변환하여 Subscriber 로 보내는 클로저를 실행하게 됩니다.
나중에 두번째 Publisher 에서 2라는 새로운 값이 생성된다면, 첫번째 Publisher 에서 보낸 이전 값과 결합하여 새로운 값인 "A 2" 를 보낼 수도 있습니다.
즉, 어떤 upstream 이 변경될 떄마다 새로운 이벤트가 발생한다는 의미입니다!
다시 돌아와서, 예제 앱에서는 3개의 upstream 을 사용하는 CombineLatest
를 사용했습니다.
3개의 스위치 모두 Boolean 값이 변경됨에 따라 하나의 Boolean 값으로 바뀌어 Play 버튼의 isEnabled
속성에 할당하게 됩니다.
당연하게 모두 true 이면 버튼이 활성화 되고 하나라도 false dㅣ라면 비활성화 되겠죠?
이처럼 간단한 동작에도 Combine 을 앱에 점짐적으로 도입할 수 있도록 설계되어 있다는 것을 볼 수 있었습니다.
Apple 에서는 모든 것을 Combine 에 맞춰 변환할 필요는 없다고 합니다.
우리의 예시 앱을 보게되면 NotificationCenter 를 사용하여 알림을 받고 값을 확인하여 작업을 수행할지 여부를 결정하는 경우 Filter 를 사용해보고
여러 비동기 작업의 결과가 모두 필요할 때는 Zip 을 사용할 수 있게됩니다.
마지막으로 URLSession 을 사용하여 데이터를 받고 JSON Decoder 를 사용하여 데이터를 변환하는 경우 Decode
라는 연산자를 사용할 수 있다고 합니다.
자 Combine 소개하기 마지막 시리즈인 Operator 까지 알아보았습니다.
가장 기본적인 내용을 다룬걸로 봐선 Combine 에는 훨씬 더 많은 내용이 있을거라 생각합니다.
그중에는 오류를 처리하거나 취소할 때, 스케줄러와 시간, Combine 을 다양한 모듈에서 사용하는 훌륭한 설계 패턴과 SwiftUI 통합이 포함된다고 합니다.
이에 대한 내용은 WWDC 19 - "Combine In Practice" 를 보면 자세한 설명이 나와있다고 합니다.
이 또한 알아볼 예정이니 다음 글도 기다려주시고 어렵고 긴 내용 읽어 주셔서 감사합니다!