티스토리 뷰

Apple/WWDC 2019

[WWDC 2019] Introducing Combine

Dev_Pingu 2021. 10. 23. 16:28
반응형

안녕하세요 Pingu입니다.🐧

 

오늘은 WWDC 2019의 "Introducing Combine"이라는 영상을 보고 정리한 글을 써보려고 합니다.

이번 영상은 2019년에 처음 공개된 Combine이라는 프레임워크를 소개하는 영상이었습니다.

 

Combine과 비슷한 역할을 하는 프레임워크로 RxSwift가 있는데, 그중 Combine 공부를 시작하기 전에 영상을 시청하게 되었네요.😀

Combine은 iOS 13.0 부터 지원하기 때문에 이전까지는 버전 문제로 선택되지 않는 경우가 있었는데 이제 iOS 15도 나온 만큼 많이 사용될 것 같습니다.

Introducing Combine

영상은 비동기 프로그래밍을 언급하며 시작됩니다.

 

마법사 학교에 학생들을 등록하는 앱을 예로 들어줍니다.😄

위와 같이 이름, 비밀번호를 입력해서 서버에 네트워크 요청을 통해 새로운 계정을 만드는 화면이 있습니다. 사용자는 이름, 비밀번호를 입력한 뒤 계정 생성 버튼을 통해 네트워크 통신을 하게 됩니다. 이 모든 과정은 메인 스레드를 차단하지 않고 처리되어야 하는데요, 해당 과정이 어떻게 진행되는지 살펴보겠습니다.

 

만약 사용자 이름을 입력하게 된다면 Target / Action을 통해 사용자 입력에 대한 알림을 받습니다. 그리고 입력한 이름이 유효한 이름인지 서버를 통해 확인하기 위해 네트워크 통신을 하는데요, 이때 Timer를 사용해서 사용자의 네트워크 요청이 서버에 과부하를 주지 않기 위해 사용자 입력이 멈출 때까지 기다립니다. 그리고 KVO 등을 통해 비동기 작업에 대한 진행률 업데이트도 수신할 수 있습니다.

 

즉 이름과 암호를 입력하면서 서버와 통신해서 유효한 값인지 체크해줘야하고, 해당 체크의 결과대로 UI도 업데이트해줘야 합니다.

 

당시에 존재했던 비동기 인터페이스는 위와 같은 것들이 있었고, 각각 다른 사용법을 가지고 있었습니다. 이것들을 함께 사용할 때 어려움이 발생했고 Combine을 만들 때 애플은 이것들을 대체하는 것이 아닌 공통점을 찾으려고 했다고 합니다. 

 

그렇게 정의한 Combine은 위와 같이 "시간 경과에 따른 값 처리를 위한 통합적이고 선언적인 API!"라고 합니다.

 

Combine Features

Combine의 특징을 살펴보겠습니다.

  • Combine은 Swift용으로 만들어져서 Generic과 같은 Swift 기능을 활용할 수 있습니다. Generic을 사용하면 코드의 양도 줄일 수 있고 하나의 알고리즘을 다양한 곳에서 사용할 수 있다는 말도 됩니다.
  • Combine은 Type safe 하므로 런타임이 아닌 컴파일 시간에 오류를 잡을 수 있다고 합니다.
  • Combine은 Composition first(구성이 먼저?)라서 핵심 개념이 간단하고 이해하기 쉽지만 이들을 사용하면 큰 효과를 볼 수 있다고 하네요.
  • Combine은 Request driven(요청 기반)이므로 앱의 메모리 사용량과 성능을 잘 관리할 수 있다고 합니다.

 

Combine Key Concepts

Combine의 핵심 개념은 위의 Publisher, Subscriber, Operator 3가지입니다.

하나씩 살펴보겠습니다~

 

Publisher

Publisher는 Combine API의 선언 부분이라고 합니다. 즉 값과 오류가 생성되는 방식을 알려줍니다. 이름과 다르게 Publisher가 반드시 뭔가를 생산하는 것은 아니라고 하네요. 구조체를 사용하기 때문에 Value 타입이며 Subscriber 등록도 허용한다고 합니다.

 

위 코드는 Publisher의 Protocol입니다. 두 개의 associatedtype이 있으며 Output은 생성하는 값의 종류, Failure는 실패했을 때 발생하는 에러입니다. 그리고 Publisher의 핵심 기능인 subscribe 함수가 있네요. 프로토콜에서 볼 수 있듯 매개변수인 subscriber의 input은 publisher의 Output과 동일해야 하고 subscriber의 failure는 publisher의 failure와 일치해야 합니다.

 

만약 Publisher가 에러를 생성할 수 없는 경우엔 Error 타입 대신 Never 타입을 사용할 수 있다고 하네요.

Never가 뭐지? 해서 공식문서에서 찾아봤는데 "정상적으로 반환하지 않는 함수의 반환 타입, 즉 값이 없는 타입"이라고 하네요.

 

간단한 예를 보면 위와 같습니다. 위 코드는 NotificationCenter를 위한 새로운 Publisher입니다. Output은 Notification이고 Failure는 Never 타입이네요. Center, name, object로 생성되는 것도 알 수 있습니다. 아까도 말했듯 Combine은 기존의 API들을 대체하는 것이 아닌 것을 볼 수 있습니다.

 

Subscriber

다음은 Subscriber입니다. Subscriber는 Publisher와 반대되는 개념으로 Publisher에게 값을 받습니다. Subscriber는 값을 받게 되면 뭔가를 처리하고 상태를 변경하기 때문에 Swift의 class와 동일한 reference type을 사용합니다.

 

Scbscriber의 protocol도 한 번 보겠습니다. 아까와 동일하게 2개의 associatedtype이 있습니다. Subscriber도 마찬가지로 에러를 수신할 수 없다면 Never 타입을 사용할 수 있다고 합니다.

 

Publisher와 다르게 3개의 함수가 있습니다. 모두 이름은 receive이며 매개변수를 살펴보면 다음과 같습니다.

  • Subscription
    • Subscriber가 Publisher에서 Subscriber에게 전달되는 데이터를 제어하는 방법입니다.
  • Input
    • Upstream에서 전달되는 값입니다.
  • completion
    • Subscribers.Completion <Failure>이라는 타입에서 볼 수 있듯 성공 혹은 실패할 수 있는 Completion을 수신할 수 있습니다.

 

Subscriber도 예제를 보면 위와 같습니다.

위와 같은 것을 Assign(할당)이라고 하며 클래스입니다. 얘가 하는 일은 Upstream에서 input을 받으면 해당 객체의 프로퍼티에 기록하는 것이라고 하네요. Swift에서는 프로퍼티 값을 작성할 때 오류를 처리할 방법이 없기 때문에 Failure를 Never로 설정해준 것도 볼 수 있습니다.

 

How Publisher, Subscriber fit together

그럼 이렇게 알아본 Publisher, Subscriber가 어떻게 동작하는지 살펴보겠습니다.

ViewController와 같은 객체에 Subscriber가 존재할 수 있고, 이를 갖고 있는 객체는 Subscriber와 함께 Publisher의 subscribe 함수를 호출해서 연결해야 하는 책임을 갖고 있습니다.

 

이 시점에서 Publisher는 Subscriber에게 subscription을 보내서 Subscriber가 Publisher에게 몇 번 혹은 무제한으로 요청을 할 수 있게 합니다.

 

그러면 이제 Publisher가 값들을 Subscriber에게 자유롭게 보낼 수 있게 되며 Publisher가 유한한 경우 completion 또는 에러를 보내게 됩니다. 여기서 중요한 것은 하나의 subscription에는 0개 이상의 값과 하나의 completion이 존재한다는 것입니다.

 

아까 마법사 학교에 학생 등록하는 예로 돌아가 보겠습니다. Wizard라는 모델 객체가 있고 grade라는 값을 갖고 있다고 해보겠습니다.

merlin이라는 애는 5학년이라고 정의합니다. 여기서 하고 싶은 것은 졸업하는 학생들에 대한 알림을 듣고 졸업하면 Wizard 객체들의 값을 업데이트하는 일입니다. 이를 위해 NotificationCenter의 Publisher와 Subscribers Assign 객체를 만듭니다. 이렇게 하면 알림을 받을 때 merlin의 grade 값을 새로 쓸 수 있습니다.

 

근데 이렇게 하면 컴파일이 되지 않는 것을 발견할 수 있는데요, 이유는 타입이 일치하지 않기 때문입니다.

 

자세히 보면 NotificationCenter는 Notification을 보내지만 Assign이 grade라는 Int 타입에 값을 쓰려면 Int가 필요하겠죠. 따라서 이를 변환해줄 무언가를 중간에 만들어줘야 합니다.

 

이 역할을 하는 것이 Operator입니다!

 

Operator

Operator는 Publisher 프로토콜을 채택하므로 Publisher와 동일하게 뭔가를 생성합니다. 선언적이므로 값 타입이며 Operator는 값을 변경하거나, 추가, 제거와 같은 다양한 동작을 처리하는 일을 합니다. 그리고 먼저 실행되는 Upstream Publisher를 subscribe 하는 일,  이후에 실행될 Downstream Subscriber에게 결과를 보내는 일을 합니다.

 

Upstream, Downstream이라는 용어는 단순하게 코드에서 위쪽에 존재하면 Upstream, 아래쪽에 존재하면 Downstream이라고 이해하면 될 듯싶었습니다. 즉 먼저 실행되는 코드를 Upstream, 이후에 실행되는 코드를 Downstream이라고 이해했습니다.

 

Operator의 예를 하나 보겠습니다. 위의 코드에서 사용된 연산자 이름은 Map입니다. Map은 upstream과 upstream의 output을 자체 output으로 변환하는 transform이 존재하는 구조체입니다. Map은 실패를 생성하지 않기 때문에 Upstream의 실패 타입을 미러링 하게 됩니다.

 

아까 봤던 문제를 해결하기 위해 이제 중간에 Map이라는 Operator가 들어옵니다. 얘가 어떤 역할을 하는지 살펴보겠습니다.

 

아까 코드에서 converter라는 녀석이 추가되었습니다. Converter의 클로저는 알림을 수신하고 NewGrade라는 userInfo를 가지고 정수이면 클로저에서 반환합니다. 이렇게 하면 클로저의 결과가 정수가 되어 Subscriber에 연결할 수 있게 됩니다. 아까 발생했던 컴파일 오류도 해결되는 것을 볼 수 있어요.

 

이를 좀 더 응용해서 위와 같은 코드도 작성할 수 있게 됩니다. 위와 같이 만들면 모든 Publisher가 사용할 수 있는 함수가 됩니다. 이렇게 하면 self를 사용할 수 있게 되므로 코드가 좀 더 간단해집니다.

 

방금 만든 것을 사용해보면 위와 같습니다. 알림을 받게 되면 앞에서 본 것과 동일한 클로저를 사용해서 매핑한 뒤 merlin의 grade 프로퍼티에 할당합니다. 이렇게 작성하고 보면 코드가 매우 선형적이고 쉬운 흐름을 갖게 되는 것을 볼 수 있습니다.

 

Assign은 cancelable 항목을 반환하는데 Cancelation도 Combine에 포함되어있습니다. 따라서 Cancelation을 사용해서 Publisher, Subscriber를 조기에 해제할 수도 있다고 합니다.

 

지금까지 본 이러한 단계가 Combine 사용 방법의 핵심입니다. 각 단계는 다음 명령어를 설명하며, 첫 번째 Publisher에서 일련의 Operator를 거쳐서 Subscriber로 값을 반환하게 됩니다.

 

Declarative Operator API

Combine에는 이미 위와 같이 많은 Operator를 가지고 있고 이들을 Declarative Operative API라고 부른다고 합니다. 잘 보면 Map, Reduce, Filter도 있고 오류 처리를 위한 Operator도 있습니다. 스레드나 큐에서 이동, from 루프 디스패치 큐, 타이머 등등 정말 많은 Operator가 있다고 하네요.

 

이렇게 Operator가 너무 많기 때문에 이를 잘 활용하는 방법을 생각하는 것이 어려울 수 있을 듯합니다.

 

Try composition first

이렇게 Operator가 많기 때문에 애플에서 권장하는 것은 Combine에 대한 핵심 디자인 원칙인 Composition으로 돌아가는 것이라고 합니다. 

 

많은 작업을 모두 수행하는 몇 개의 Operator를 제공하기보다는 간단한 작업을 수행하는 Operator를 많이 제공해해서 이해하기 쉽게 만들었다는 의미로, 개발자들이 쉽게 이해할 수 있도록 Swift Collection API에서 이름을 많이 따왔다고 하네요.

 

위 그림을 보면 왼쪽에는 Synchronous(동기) API가 있고 오른쪽에는 Asynchronous(비동기) API가 있습니다. 그리고 위쪽에는 단일 값, 아래쪽에는 여러 개의 값이 있습니다.

 

Swift에서 하나의 정수를 동기적으로 처리할 경우엔 Int를 사용하면 되지만 Combine을 사용해서 비동기적으로 사용하려면 Future를 사용하면 된다고 하네요. 또한 여러개의 값을 동기로 사용할 때는 Array, 비동기로 사용할 거면 Publisher를 사용하면 된다고 합니다.

 

예를 하나 보겠습니다. 위의 코드를 보면 map에서 publisher에서 전달된 값이 Int 값이 아니라면 0을 저장하는 코드입니다. 그런데 값 자체가 이상한 건데 0이라는 값이 저장되는 게 마음에 들지 않기 때문에 nil을 반환할 수 있도록 수정하고 싶습니다.

 

이때 사용할 수 있는 것은 Swift 4.1에 도입된 compactMap입니다. compactMap도 Combine에서 사용할 수 있기 때문에 위와 같이 사용해주면 됩니다. 그럼 이제 코드는 아까와 다르게 Int 값이 아닌 값은 nil을 downstream에 반환해줍니다.

 

이번에는 Swift에서 자주 사용했던 filter도 사용해보겠습니다. Array에서 사용하던 filter와 마찬가지로 위와 같이 사용하면 upstream에서 전달된 값이 5 이상인 값만 downstream으로 전달해줍니다.

 

이번에는 prefix라는 Operator를 사용해봅니다. 위 코드에서 .prefix(3)은 위 코드가 반복될 때 3번까지만 값을 수신하라는 의미가 됩니다. 즉 3번까지만 전달된 값을 downstream으로 전달하라는 말이 됩니다.

 

결론적으로 위 코드는 어떤 이벤트가 발생하면 전달된 값이 Int 타입이고 5 이상이며 수신받은 횟수가 3회 이하일 때만 merlin의 grade 프로퍼티에 전달된 값을 저장하는 코드가 됩니다.

 

Combining Publishers

지금까지 살펴본 Map, Filter는 주로 동기 작업을 위한 API 였습니다. 하지만 Combine은 비동기 작업을 할 때 빛을 발하기 때문에 이를 위한 Zip, CombineLatest라는 Operator도 살펴보겠습니다.

 

Zip

계속해서 예를 들던 마법사 계정 만들기 앱에서 이번에는 지팡이를 생성하는 단계를 보겠습니다. 지팡이를 만들기 위해서는 3개의 오래 걸리는 비동기 작업이 완료되어야 한다고 하네요. 따라서 3개의 작업이 모두 완료될 때 continue 버튼을 활성화하고 싶다고 합니다. 이럴 때 zip을 사용하면 된다고 합니다!

 

Zip은 여러 개의 upstream input을 하나의 tuple로 변환해서 downstream으로 전달합니다. 이때 downstream으로 전달하기 위해서는 tuple의 모든 값이 입력되어야 하므로 upstream의 모든 input이 전달된 후에 downstream으로 tuple을 전달할 수 있게 됩니다.

 

위의 그림에서 보면 (A, 1)이라는 튜플이 subscriber로 전달되는데, A, 1을 모두 받은 뒤에 이를 tuple로 만들어서 subscriber로 전달한다는 의미입니다. A 혹은 1 중 하나라도 받지 못하면 subscriber는 아무것도 전달받지 못하게 되겠죠?

 

이를 아까 지팡이 만들던 예에 적용해보면 위와 같습니다. 3개의 upstream이 필요한 Zip을 사용해서 3개의 비동기 작업이 완료되어야 downstream으로 값을 전달할 수 있습니다. 모두 완료되면 continueButton의 isEnabled 값에 true가 저장되므로 버튼이 활성화됩니다! 이렇게 다음 작업으로 넘어갈 수 있게 됩니다!

 

다음 작업은 지팡이를 가지고 놀기 전에 숙지해야 할 규칙들에 동의해야 하는 작업이라고 합니다. 😃 Play 버튼이 활성화되기 전에 세 개의 스위치를 모두 활성화해야 하고, 하나라도 비활성화된 다면 play 버튼도 비활성화해야 합니다. 이럴 때 사용할 수 있는 것이 CombineLatest입니다.

 

upstream에서 전달받은 값을 하나의 값으로 변환하는 것은 Zip과 동일하지만 upstream의 값이 변경될 때마다 downstream으로 값을 전달합니다. 이렇게 해서 downstream은 upstream의 가장 최신 정보를 얻을 수 있습니다.

 

이를 코드에 활용하면 위와 같습니다. 3개의 upstream을 사용하는 CombineLatest를 사용해서 3개의 스위치가 모두 활성화되었을 때만 true를 downstream에 전달해주고 다른 경우에는 모두 false를 전달하게 됩니다. 이렇게 하면 Play 버튼의 활성화 여부를 쉽게 관리할 수 있게 됩니다.

 

이렇게 해서 영상에서는 Combine에 대한 소개를 마칩니다. Combine을 사용하기 위해서 기존 코드를 모두 변환할 필요는 없고 이번 영상에서 봤듯 NotificationCenter의 확장된 형태로 사용할 수 있었습니다. 그리고 간단하게 여러 Operator의 사용 예도 봤었습니다.

 

비동기 작업 중 가장 많이 하는 작업 중 하나는 네트워크 작업일 텐데요, URLSession을 사용해서 데이터를 수신한 뒤 JSON Decoder를 사용해서 데이터를 변환하는 경우에 사용할 수 있는 decode Operator도 있다고 하네요.

 

좀 더 자세한 내용은 아래 영상을 참고하라고 합니다.

이렇게 이번 영상은 마무리됩니다.

 

이번 영상에서는 Combine의 가장 중요한 Publisher, Subscriber, Operator가 무엇인지 알아봤고, 그중 몇 가지 Operator에 대해서도 알아봤습니다. Combine을 공부하기 전에 핵심 개념을 알아보기에 좋은 영상이었던 것 같아요.

 

그럼 오늘 WWDC 영상인 "Introducing Combine"는 여기까지 정리하도록 하겠습니다.

 

감사합니다 😀

 

반응형

'Apple > WWDC 2019' 카테고리의 다른 글

[iOS 앱개발] Multiple Window를 위한 SceneDelegate  (1) 2020.11.11
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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
글 보관함