[WWDC19] Introducing Combine
아아-🎤
오늘은 WWDC19의 "Introducing Combine" 이라는 영상을 보고 정리해보려고 합니당
Combine은 iOS 13부터 지원하고 있고 SwiftUI랑 찰떡궁합이라고 하더라구용?
SwiftUI 쓰는 사람으로써 꼭 공부해봐야지!! 했는데 이제서야 보네용
요즘 공부 스타일을 잡아가는 중인데 확실히 그냥 구글링해서 공부하기 보단 애플 공식 문서랑 WWDC 많이 보는게 더 좋은 것 같아요
아무튼! 이번 기회에 Combine 기본 개념을 확실히 잡고 가보자구욘
정리 시작해보겠습니다람쥐🐿️
Combine을 소개하기 전에,
예시로 마법사 학교에 학생들을 등록하는 앱을 보여주고 있음
이름, 비밀번호를 입력해서 서버에 네트워크 요청을 통해 새로운 계정을 만드는 화면임
사용자는 이름, 비밀번호를 입력한 뒤 Create Account 버튼을 통해 네트워크 통신을 하게 됨
이 모든 과정은 메인 스레드를 차단하지 않고 처리되어야 함
해당 과정이 어떻게 진행되는지 알아보겠음
사용자 이름을 입력하게 되면 Target/Action을 통해 사용자 입력에 대한 알림을 받음
입력한 이름이 유효한 이름인지 서버를 통해 확인하기 위해 네트워크 통신을 수행함
이때, Timer를 사용해서 사용자의 네트워크 요청이 서버에 과부하를 주지 않기 위해 사용자의 입력이 멈출 때까지 기다림
그리고 KVO 등을 통해 비동기 작업에 대한 진행률 업데이트도 수신할 수 있음
이름과 비밀번호를 입력하며서 서버와 통신해서 유효성 체크를 해줘야 하고 체크한 결과대로 UI도 업데이트 해줘야 함
당시에 존재한 비동기 인터페이스는 아래 와 같음
이들은 각각 다른 사용법을 가지고 있었고 이것들을 함께 사용할 때 어려움이 발생함
그래서 애플은 Combine을 만들 때 이것들을 대체하는 것이 아닌 공통점을 찾으려 함
이것이 바로 "시간 경과에 따른 값 처리를 위한 통합적이고 선언적인 API"인 Combine임!
Combine
Combine의 특징은 아래와 같음
Combine Features
- Generic
Combine은 Swift용으로 작성됨. 그래서 Generic과 같은 Swift 기능을 활용할 수 있음. Generic을 사용하면 작성해야하는 코드의 양을 줄일 수 있음. 이는 또한 비동기 동작에 대한 알고리즘을 한 번 작성하고 모든 종류의 다양한 비동기 인터페이스에 적용할 수 있음을 의미!
- Type safe
Type safe 하므로 런타임이 아닌 컴파일 타임에 오류를 잡을 수 있음
- Composition first
구성이 먼저! 핵심 개념이 단순하고 이해하기 쉽지만 이를 하나로 합치면 더 많은 것을 만들 수 있다는 것을 의미
- Request driven
요청 기반이므로 앱의 메모리 사용량과 성능을 더욱 주의깊게 관리할 수 있는 기회 제공
다음으론 Combine의 핵심 개념을 소개해보겠음
Key Concepts
- Publishers
Combine API의 선언적인 부분. 값과 오류가 생성되는 방식을 알려줌(반드시 뭔가를 생성하는 것은 아님)
구조체를 사용하기 때문에 값 타입. Publisher는 Subscriber 등록도 허용. 시간이 지남에 따라 값을 받는 것.
Output: 생성하는 값의 종류
Failure: 생성되는 오류의 종류(Publisher가 오류를 생성할 수 없는 경우 해당 관련 유형에 대해 Nerver 타입 사용할 수 있음)
* Never > 정상적으로 반환하지 않는 함수의 타입, 즉 값이 없는 타입
Subscribe라는 하나의 핵심 기능이 있음
Subscribe에서는 위 프로토콜에서 볼 수 있듯 매개변수 subscriber의 Input은 Publisher의 출력과 일치해야하고 subscriber의 failure가 Publisher의 failure와 일치해야 함
Example) NotificationCenter의 새로운 Publisher(Combine은 기존의 API를 대체한 것이 아닌 것을 볼 수 있음)
구조체이고 Ouput 유형은 Notification / Failure 타입은 Never
center, name, object 세가지 항목으로 생성됨
- Subscribers
Publisher와 짝(?)이 되는 개념으로, Publisher에게 completion을 포함하여 값을 받음
Subscribers는 일반적으로 값을 수신하면 상태를 실행하고 변경하기 때문에 Swift의 class와 동일한 참조 타입을 사용함
Input
Failure: (Subscriber도 마찬가지로 실패를 수신할 수 없는 경우 Never 타입 사용 가능)
세가지 주요 기능
subscription(구독)을 받을 수 있음
- Subscription: Subscriber가 Publisher에서 Subscriber로의 데이터 흐름을 제어하는 방법
- Input: Upstream에서 전달되는 값
- Comletion: Subscribers.Completion<Failure>이라는 타입에서 볼 수 있듯 성공 혹은 실패할 수 있는 Completion 수신
Example) Subscriber
Assign은 클래스이며 클래스 인스턴스, 객체 인스턴스 및 해당 객체에 대한 type safe key path로 초기화됨
Upstream에서 Input(입력)을 받으면 해당 객체의 해당 프로퍼티에 기록하는 것
Swift에서는 프로퍼티 값을 작성할 때 오류를 처리할 방법이 없기 때문에 실패 유형인 Assign을 Never로 설정
Publisher, Subscriber Flow
1. ViewController와 같은 객체에 Subscriber가 존재할 수 있고, 이를 가진 객체는 (Subscriber)와 함께 Publisher의 subscriber 함수를 호출해서 연결해야 하는 일을 담당하게 됨
2, 3. Publisher는 Subscriber에게 subscription을 보내서 Subscriber가 Publisher에게 몇번 혹은 무제한으로 요청할 수 있게 함
4, 5. Publisher가 값들을 Subscriber에게 자유롭게 보낼 수 있게 되며 Publisher가 유한한 경우 completion 또는 에러를 보냄
* 다시 말하자면, 하나의 subscription은 0개 이상의 값 및 completion이 존재함
Example)
Wizard라는 모델 객체가 있고 우리의 관심은 해당 마법사가 어떤 학년에 속해있는지 뿐이다.
현재 5학년인 Merlin부터 시작해보겠음
내가 원하는 것은 학생들의 졸업에 대한 알림을 수신하는 것임
학생들이 졸업하면 모델 객체의 값을 업데이트하고 싶음
그래서 Merlin 졸업에 관한 NotificationCenter의 Publisher를 만들고, Subscribers Assign 객체를 생성해 Merlin의 grade 프로퍼티에 값을 업데이트하도록 함
subscribe를 사용하여 연결!
하지만 예상한대로 컴파일 되지 않았음
왜냐?
타입이 일치하지 않기 때문.
NotificationCenter은 Notification을 보내지만 Assign은 grade라는 Int 타입에 쓰려면 정수가 필요함
따라서 우리에게 필요한 것은 알림과 정수 사이를 변환시켜줄 무언가가 있어야 함
그것이 바로 Operator!!!
- Operators
Publisher 프로토콜 채택(Publisher와 동일하게 뭔가를 생성함)
선언적이므로 값 유형, 값 변경/추가/제거 또는 다양한 종류의 동작에 대한 처리
Upstream Publisher를 subscribe하고 그 결과를 Downstream Subscriber에게 결과를 보냄
Example) Operators
Map은 연결되는 Upstream과 Upstream의 Output을 자체 Output으로 변환하는 transform이 존재하는 구조체임
Map은 실패를 생성하지 않기 때문에 Upstream의 실패 타입을 미러링하여 통과 시킴
따라서 Map을 사용하면 Notification과 Int 사이를 변환할 수 있음
아까 코드에서 converter라는 애가 추가 됨
converter의 클로저는 Notification을 받고 "NewGrade"라는 userInfo key를 찾음
그것이 존재하고 정수라면, 클로저에서 반환함
없거나 정수가 아닌 경우 기본값 0을 사용함
이것이 의미하는 바는 무슨일이 있어도 이 클로저의 결과는 정수이므로 이를 Subscriber에 연결할 수 있다는 것!
모든 것이 연결되고, 컴파일되고 작동하게 됨
구문이 장황해질 수 있으니 아래와 같은 코드도 작성할 수 있음
Publisher 프로토콜의 extension으로 모든 Publisher가 사용할 수 있게 됨. 간단히 self를 사용할 수 있음.
방금 만든 것을 새롭게 적용해보면 아래와 같음
Notification을 받으면 앞서 본것과 동일한 클로저를 사용하여 매핍한 다음 Merlin의 grade 프로퍼티에 할당함
Assign은 cancelable 항목을 반환하는데 이 기능도 Combine에 포함되어 있음
Cancelation을 사용하면 필요한 경우 Publisher Subscriber의 순서를 조기에 해제할 수 있음
이러한 단계별 구문은 실제로 Combine 사용방법의 핵심이라 할 수 있음
각 단계의 체인은 다음 명령을 설명하고,
첫 번째 Publisher에서 일련의 Operator를 거쳐 Subscriber를 끝으로 값을 변환하게 됨
Declarative Operative API
Combine에는 많은 Operator를 가지고 있으며 이들을 Declarative Operative API라고 부름
Functional transformations: Map과 같은 기능적 변환
List operations: Filter, Reduce와 같은 목록 작업
Error handling: 오류를 기본 값이나 대체 값으로 바꾸는 것과 같은 오류 처리
Thread or queue movement: 무거운 처리 작업을 백그라운드 스레드로 이동하거나 UI작업을 메인 스레드로 이동
Scheduling and time: from loop, dispatch queue, timer 지원, timeout 등
이렇게 사용할 수 있는 Operator가 너무 많기 때문에 어떤걸 활용할지 부담스러울 수 있음
Try composition first
Apple에서 권장하는 방법은 Combine의 핵심 디자인 원칙, 즉 Composition으로 돌아가는 것
많은 일을 하는 몇몇 Operator를 제공하기보다는 각각 조금씩만 수행하는 많은 Operator를 제공하여 이해하기 쉽게 만들 것
이러한 모든 Operator를 탐색하는 데 도움을 주기 위해 Swift Collection API에서 이름을 많이 따옴
사분면 그래프를 그려본다
한 쪽에는 동기식 API가 있고 다른 쪽에는 비동기식이 있음
상단에는 단일 값이 있고 하단에는 많은 값이 있음
Swift에서는 정수를 동기적으로 표현해야하는 경우 Int와 같은 것을 사용하면 되지만
많은 정수를 동기적으로 표현해야하는 경우엔 정수 배열과 같은 것을 사용함
Combine에서는 이러한 개념을 가져와 비동기 세계에 매핑함
따라서 단일 값을 비동기식으로 표현해야 하는 경우엔 Future라는 것을 사용함
많은 값을 비동기적으로 표현해야하는 경엔 Publisher 사용
즉, 배열을 사용하는 방법을 이미 알고 있는 특정 종류의 작업을 찾고 있다면 Publisher에서 해당 이름을 사용해 봐라!
Example)
아까 코드에서 키가 없거나 정수가 아닌 경우 기본값 0을 사용하기로 했는데,
이 잘못된 값(키가 없거나 정수가 아닌 경우)이 내 모델 개체에 기록되는 것을 허용하지 않는 것이 더 나은 아이디어 일 수 있음
그래서 내가 할 수 있는 한 가지는 이 클로저가 nil을 반환하도록 허용한 다음 nil값을 필터링 하는 것!
Swift 4.1에서 해당 작업에 대한 이름을 도입했다고 하는데 이를 CompactMap이라고 한다고 함
CompactMap도 Combine에서 사용할 수 있음
적용해보면 아래와 같음
그럼 아까와는 다르게 nil을 반환하면 CompactMap은 이를 필터링하여 downstream으로 진행되지 않도록 함
Swift에서 자주 사용한 filter를 사용해보겠음
우리 학교에는 5학년 이상의 학생들만 입학이 허용된다고 가정
필터를 사용하면 upstream에서 전달된 값이 5 이상인 값만 downstream에 전달해줌
이번엔 졸업은 최대 3번까지만 허용된다고 가정해보겠음
배열에서 처음 세 요소를 가져와야 하는 경우 prefix(3)을 사용할 수 있음
이는 세 개의 값을 받은 후 upstream을 취소하고 downstream으로 완료를 보내는 것
결론적으로 Merlin의 졸업식을 수신하는 NotificationCenter Publisher가 있음
그가 졸업하면 해당 프로퍼티, 해당 Notification에서 NewGrade를 가져올 것임
그런 다음 Int 값이 5보다 크고 수신 받은 횟수가 3번 이하인 경우에만 grade 프로퍼티에 전달된 값을 저장하는 코드가 됨
지금 까지 살펴본 Map, Filter는 주로 동기 작업을 위한 API 였음
하지만 Combine은 비동기 작업을 할 때 빛을 발하기 때문에 Zip, CombineLastest라는 Operator도 살펴볼거임
- Zip
계정 생성하는 부분에서 사용자가 Continue하도록 허용되기 전에 지팡이가 생성될 때까지 기다려야 한다고 가정
세 가지 장기 실행 비동기 작업이고 세 가지 작업이 모두 완료되면 Continue 버튼이 활성화 됨
이것이 Zip의 역할임!
Zip은 여러 upstream 입력을 단일 튜플로 변환해 downstream 으로 전달함
이때 downstream으로 전달하기 위해서는 tuple의 모든 값이 입력되어야 하므로 upstream의 모든 input이 전달된 후에 downstream으로 tuple을 전달할 수 있게 됨
예를 들어 첫 번째 Publisher가 A를 생성한 다음 두 번째 Publisher가 1을 생성하면 튜플을 생성하고 모든 값이 입력됐으므로 해당 값을
Subscriber에게 downstream으로 보낼 수 있음
예시 앱을보면 각각의 Bool 결과를 제공하는 세 가지 비동기 작업이 완료되어야 downstream으로 전달할 수 있음
튜플을 단일 Bool로 매핑하고 isEnabled 값에 true가 저장되므로 Continue 버튼이 활성화 됨
다음 작업은 지팡이를 가지기 놀기 전에 이용 약관에 동의해야함
이는 Play버튼을 활성화하기 전에 이 세 가지 버튼이 모두 활성화 되어야 함
세 가지 버튼 중 하나가 나중에 비활성화 되면 Play 버튼을 비활성화 해야함
이것은 Combine Late 작업임
- Combine Lastest
Zip과 마찬가지로 여러 upstream 입력을 단일 값으로 변환함
그러나 Zip과 달리 진행하려면 upstream의 입력이 필요하므로 일종의 When/or 작업이 됨
이를 지원하기 위해 각 upstream에서 받은 마지막 값을 저장함
또한 이를 단일 downstream 값으로 변활할 수 있는 클로저로 구성됨
예를 들어 첫 번째 Publisher가 A를 생성한 다음 두 번째 Publisher가 A1을 생성하면 이를 문자열화해서 downstream으로 보내는 클로저를 실행함
나중에 두번째 Publisher가 새 값을 생성하면 첫 번째 Publisher의 이전 값(A)와 결합하여 새 값을 downstream으로 보낼 수 있음
upstream이 변경되면 새로운 이벤트가 발생한다는 의미!
예제 앱에서는 3개의 upstream을 사용하는 CombineLast 버전을 사용함
즉, 3개의 스위치 모두 Bool 상태가 변경됨에 따라 이를 다시 단일 Bool값으로 변환하고 이를 Play 버튼의 isEnabled 프로퍼티에 저장함
즉, 둘 중 하나라도 거짓이면 결과는 거짓임
그러나 모두 참이면 결과는 참이므로 버튼은 활성화됨
Try It
Combine을 사용할 수 있는 곳
NotificationCenter를 사용하고 Notification을 받은 다음 그 내용을 살펴보고 조치를 취할지의 여부를 결정하는 경우 > Filter
여러 비동기 작업의 결과에 가중치를 부여하는 경우 네트워크 작업을 포함하여 Zip을 사용할 수 있음
URLSession을 사용해서 데이터를 수신한 뒤 JSON Decoder를 사용해서 데이터를 변환하는 경우에 사용할 수 있는 decode Operator도 있음
이렇게 WWDC19 Introducing Combine을 봐봤는데 Combine 공부하기 전에 잘 본 것 같아용
핵심 개념이 정리된 느낌..! 앞으로 더 깊숙히 공부해보겠습니다람쥐🐿️
📚 WWDC19 Introducing Combine: https://developer.apple.com/videos/play/wwdc2019/722