WWDC/iOS

[iOS] WWDC19 - Introducing Combine

유정주 2023. 1. 22. 19:41
반응형

App Requirements

몇 가지 요구사항을 만족하는 앱을 만들고 있습니다.

사용자 이름을 입력하고, 서버에 전달해서 유효한 입력인지 확인합니다.

서버 통신을 할 때는 메인 스레드를 Blocking 하지 말고, 인터페이스를 그대로 유지해야 합니다.

 

이부분에서 Target/Action, Timer, KVO 등이 사용됩니다.

사용자가 TextField에 입력을 하고, URLSession을 이용해 서버와 통신합니다.

네트워크 리소스를 아끼기 위해 Timer를 이용해 사용자가 입력이 멈춘 뒤 서버에 요청을 보낼거고,

네트워크 Progress Update를 위해 KVO를 통해 처리할 수 있습니다.

 

결과적으로 이러한 비동기 API들을 만나게 됩니다.

이들은 각각 사용하는 방법이 달라서, 엮어서 쓰는 것은 까다롭고 문제가 생길 가능성이 높습니다.

 

이 문제를 해결하기 위해 애플은 위의 API에서 공통점을 추출하여,

Combine을 만들었습니다.

 

Combine

Combine은 시간 흐름에 따라 처리하는 선언적 API 입니다.

 

Combine은 네 가지 특징을 가지고 있습니다.

Generic, Type safe, Composition first, Request Driven 입니다.

 

Generic과 같은 Swift 기능을 사용해 동일한 코드를 중복 없이 활용할 수 있습니다.

Type safe는 컴파일 타임에 에러를 잡을 수 있습니다.

Composition first는 간단하고 이해하기 쉬운 컨셉을 가졌지만, 여러 개를 구성하고 조합하여 쉽고 편리하게 사용할 수 있습니다.

Request driven은 필요할 때 요청해서 불필요한 리소스 낭비를 줄입니다.

 

Combine Key Concepts

Combine의 핵심 개념은 Publisher, Subscriber, Operator입니다.

 

Publisher

Combine의 Publisher는 Value와 Error가 어떻게 생성될 것인지 정의합니다.

값 타입으로 선언되어 있으며,

Subscriber의 등록을 받습니다.

 

Publisher는 두 개의 assocatedType을 가지고 있습니다.

Output은 결과고, Failure는 에러 타입입니다.

만약 에러를 줄 수 없는 경웨는 Failure로 Never를 사용하면 됩니다.

 

Publisher는 subscribe이라는 딱 하나의 함수를 가지고 있습니다.

이 함수를 사용하기 위해서는

Publisher와 Publisher의 구독을 받는 Subscriber의 Input, Failure 타입이 같아야 합니다.

 

NotificationCenter는 기본적으로 위와 같은 extension이 추가되어 있습니다.

Output 타입은 Notification, Failure 타입은 Never입니다.

 

Subscriber

Subscriber는 Publisher와 반대되는 개념입니다.

Publisher가 값을 만들어내면, Subscriber는 그 값을 받습니다.

또, Publisher가 값을 방출하는 것이 끝났을 때 하는 행동을 정의할 수 있습니다.

 

Subscriber는 참조 타입이고, Class로 구현되어 있습니다.

 

Subscriber에도 두 개의 associatedType이 있는데요.

하나는 Input, 하나는 Failure 입니다.

에러 타입이 없는 경우 Failure를 Never로 설정하면 됩니다.

 

이 associatedType은 등록할 Publisher와 타입이 같아야 합니다.

 

Subscriber는 3개의 receive 함수를 가지고 있습니다.

첫 번째 receive 함수는 Subscription을 전달 받습니다.

Subscription은 Publisher가 방출한 데이터를 Subscriber로 어떻게 제어할 것인가에 대한 흐름입니다.

 

두 번째 receive 함수는 Input을 전달 받고,

 

마지막 receive 함수는 Publisher의 방출이 끝날 때의 동작을 정의한 completion을 전달 받습니다.

 

Subscriber의 예시 중 하나는 Assign입니다.

Input 타입으로 값을 받아서 그 값을 keypath를 통해 찾아 적용하는 역할입니다.

단순히 값을 쓰는 동작이기 때문에 에러를 정의하지 않으려고 Never로 Failure를 설정했습니다.

 

The Pattern

Publisher와 Subcriber의 동작을 알아봅시다.

Publisher 객체를 생성하고,

Subscriber를 생성한 후 Publisher의 subscribe 매개변수로 전달됩니다.

그럼 Publisher 구독이 된 것입니다!

 

구독이 되면 Publisher는 subscription을 Subscriber에게 전달합니다.

이 Subscription은 Subscriber가 Publisher로부터 원하는 값을 요구하거나, 구독을 취소하는데 사용됩니다.

 

세팅이 완료되면 Subscriber는 Publisher에게 원하는 개수만큼의 Request를 하게 됩니다.

Publisher는 Subscriber가 요청한 개수만큼 receive 메서드를 통해 데이터를 전달합니다.

 

만약 Publisher가 finite 하면, (모든 데이터를 전달했다면)

Subscriber의 receive(completion:)을 호출하고 통신이 종료됩니다.

 

Comeback to Wizard

기존에 만들던 앱으로 다시 와봅시다.

merlin이라는 Wizard 객체를 추적하면서 값을 변경해줄건데요.

NotificationCenter가 새로운 값을 userInfo에 넣어줄거고, 이를 반영해주는 코드를 짜려고 합니다.

 

Wizard라는 grade 5의 merlin 인스턴스를 만들고,

NotificationCenter를 이용해 Publisher 객체를 생성합니다.

값을 받아서 반영해야 하니, Subscribers의 Assign으로 merlin을 등록하고,

Publisher를 구독합니다.

 

하지만 이 코드는 동작하지 않습니다!

 

Publisher의 Output, Failure 타입과 Subscriber의 Input, Failure 타입이 다르기 때문입니다.

NotificationCenter의 Publisher Output 타입은 Notification이고,

Assign의 Input 타입은 Int 이죠.

 

결국 타입이 맞지 않아 컴파일 에러가 발생합니다.

 

Operator

이 문제는 Operator를 이용해 해결이 가능합니다.

Publisher와 Subscriber 사이에 Operator가 들어가서 타입을 맞춰주는 것이죠.

 

Operator는 다음과 같은 특징을 가집니다.

Operator는 Publisher를 채택하고 있습니다. 하위로 값을 다시 보내야 하기 때문입니다.

값의 변화를 위한 동작을 가지고 있고,

Publisher를 subscribe 하고, Subscriber에게 결과를 전달하는 중간 다리 역할을 합니다.

마지막으로, Operator는 값 타입입니다.

 

Map Operator를 보면 Publisher 안에 정의되어 있는 것을 볼 수 있습니다.

Upstream으로 Publisher 타입을 받고, Output 타입을 반환합니다.

여기서 Failure는 Publisher의 Failure 타입과 동일해야 합니다.

 

Map을 이용하면,

이렇게 중간에 타입을 변환해줄 수 있는거죠.

 

코드로 보면 이렇습니다.

Publisher의 Map은 upstream으로 Publisher 타입을 받고,

converter에게 Int 타입 값을 반환합니다.

 

converter는 subscribe 메서드로 gradeSubscriber를 등록하는데,

converter의 Publisher의 Output 타입과 gradeSubscriber의 Input 타입이 Int로 동일해져서

성공적으로 동작하게 됩니다.

 

Operator Construction

하지만 저렇게 불편하다면,

아무도 Combine을 사용하지 않겠죠?

 

그래서 애플은 Publisher의 extension으로 map이라는 함수를 만들어서,

Output을 어떻게 변경할지에 대해서만 작성하면

Publisher.map Operator를 반환하게 했습니다.

 

심지어 간단한 Subscriber도 extension으로 구현되어 있어서,

따로 객체를 생성해서 등록하지 않아도 됩니다.

 

이 두 가지를 이용하면

이렇게 간단하게 원하는 동작을 수행할 수 있습니다.

 

이 코드는 직관적이라 이해하기 쉽고,

하나하나 수행되기 때문에 흐름을 파악하기도 쉽습니다.

 

Declarative Operator API

Combine은 수많은 Operator를 가지고 있습니다.

 

여기서 살펴봤던 map 뿐만 아니라

 

이렇게나 많이요.

 

따라서 이 모든 것을 한 번에 이해하려고 하는 것은 벅찰 수 있습니다.

 

Try composition first

발표 초반에 Combine은 Composition first 특징을 가지고 있다고 말했습니다.

 

설계부터 많은 작업을 수행하는 Operator를 제공하는 대신,

작은 핵심 기능만 수행하는 Operator를 많이 제공하여 이해하기 쉽도록 구성했습니다.

개발자들이 쉽게 이해할 수 있도록 Swift Collection API에서 이름을 많이 따왔다고 합니다.

왼쪽은 동기 API이고, 오른쪽은 비동기 API 입니다.

또, 위쪽은 단일 값, 아래쪽은 다수의 값을 다룹니다.

 

단일 값을 비동기적으로 나타내야 하면 Future 타입을 사용하고,

다수의 값을 비동기적으로 나타내야 하면 Publisher 타입을 사용하면 됩니다.

 

예를 들어 봅시다.

이 코드는 키가 없거나 정수가 아닌 경우 0을 반환하고 있습니다.

임의의 값인 0을 저장하는 것보다, nil을 반환하도록 하는 것이 더 좋을 수 있습니다.

 

map 대신 compactMap을 사용하면 Int 값이 존재하는 경우의 값만 downstream 되게 합니다.

 

여기에서 5학년 이상만 필요하다면,

filter를 이용하면 됩니다.

 

여기에 앞의 세 개의 데이터만 downstream을 시킨다면,

prefix 연산자를 사용하면 됩니다.

 

Combining Publishers

두 개의 Operator를 더 알아보겠습니다.

Zip과 CombineLatest 입니다.

 

Zip

샘플 앱에는 지팡이를 만드는 동작이 있습니다.

이 동작은 세 가지 비동기 처리가 모두 완료될 때 수행됩니다.

Zip Operator를 이용하면 3개의 작업이 모두 완료되는 시점을 Catch 하여 Continue 버튼을 활성화할 수 있습니다.

 

Zip은 다수의 Upstream을 하나의 tuple로 변환합니다.

Downstream으로 값을 전달하고 작업을 진행하려면 모든 Upstream에서 Input이 필요합니다.

첫 번째 Publisher에서 A를 생성하고,

두 번째 Publisher에서 1을 생성하면,

Zip을 거쳐서 (A, 1) 튜플이 생성되어 Subscriber에게 Downstream으로 보낼 수 있습니다.

 

따라서 Zip을 이용하면

organizing, decomposing, arranging이 모두 true일 때

isEnabled가 true가 되어 continue 버튼이 활성화 됩니다.

 

Combine Latest

샘플 앱에서는 세 가지 토글 버튼이 모두 활성화 돼야 Play 버튼이 활성화 됩니다.

하나라도 비활성화 되었다면 Play 버튼도 비활성화 해야 합니다.

이럴 때 CombineLatest를 사용할 수 있습니다.

 

Upstream에서 전달받은 값을 하나의 값으로 변환하는 것은 Zip과 동일하지만,

Upstream 값이 변경될 때마다 Downstream으로 값을 전달합니다.

이렇게 해서 Downstream은 Upstream의 가장 최신 정보를 얻을 수 있습니다.

 

read, practiced, approved가 모두 true 여야만

isEnabled가 true로 설정되어 playButton이 활성화 됩니다.

 

마무리

모든 것을 한 번에 Combine으로 바꾸는 것은 어려울 수 있습니다.

일단 위 세 가지부터 Combine으로 변경해보라고 하네요.

 

또, 대표적인 비동기 동작 중 하나인 URLSession을 이용한 네트워크 작업에서 사용할 수 있는

decode Operator도 소개하고 있습니다.

 

더 자세한 내용은 "WWDC19 - Combine in Practice"를 참고하라고 합니다.

이 영상은 다음 포스팅으로 정리하겠습니다.

 

감사합니다!


아직은 초보 개발자입니다.

더 효율적인 코드 훈수 환영합니다!

공감 댓글 부탁드립니다.

 

 

 

 

반응형