Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introducing Combine #23

Open
Taehyeon-Kim opened this issue Aug 9, 2022 · 1 comment
Open

Introducing Combine #23

Taehyeon-Kim opened this issue Aug 9, 2022 · 1 comment
Assignees
Labels
WWDC19 wwdc2019

Comments

@Taehyeon-Kim
Copy link

Combine is a unified declarative framework for processing values over time. Learn how it can simplify asynchronous code like networking, key value observing, notifications and callbacks.

@Taehyeon-Kim Taehyeon-Kim added the WWDC19 wwdc2019 label Aug 9, 2022
@Taehyeon-Kim Taehyeon-Kim self-assigned this Aug 9, 2022
@Taehyeon-Kim
Copy link
Author

Introducing Combine

combine1

WWDC19에는 Apple의 최신 Framework인 Combine을 소개합니다. 비동기 프로그래밍에 대해서 이야기하자고 하면서 서막을 여는데요. 같이 예시를 보고 이해하면서 Combine에 대해서 알아보도록 합시다.

combine2

간단한 회원가입 화면을 예시로 듭니다. 이 화면에서의 요구사항은 크게 다음과 같습니다. 우선 사용자 이름이 유효한지 네트워크에 요청을 해서 확인을 합니다. 그리고 비밀번호가 서로 일치하는지 확인을 합니다. 이 모든 작업은 메인 스레드에서 차단되지 않고 반응형 인터페이스를 유지해야 합니다.

스크린샷 2022-08-09 오후 6 35 50

먼저 사용자 이름을 입력하기 시작합니다. 여기서도 이미 많은 비동기 작업이 진행되고 있습니다. Target/Action을 사용하여 사용자 입력에 대한 알림을 수신했습니다. 여기서 많은 네트워크 요청으로 서버를 overwhelm하지 않기 위해서 Timer를 사용하여 사용자가 입력을 잠시 멈출 때까지 기다립니다. 그리고 KVO와 같은 것을 사용해서 해당 비동기 작업에 대한 진행률 업데이트를 수신합니다.

이름과 암호를 입력하면서 서버와 통신하고 유효한 값인지를 체크해줍니다. 그리고 그에 따라 UI도 업데이트 해주게 됩니다.

Asynchronous Interfaces

  1. Target/Action
  2. Notification center
  3. URLSession
  4. Key-value observing
  5. Ad-hoc callbacks

Cocoa SDK 전반에 걸쳐서 비동기 인터페이스는 정말 상당히 많습니다. 이 모든 것들은 각각 중요하고 각기 다른 사용법을 가지고 있습니다. 이것들을 함께 사용할 때 어려울 수 있습니다. 이 모든 것들을 대체하지 않고, 공통점을 찾기 위해 Combine이 등장하게 됩니다.

Combine

A unified, declarative API for processing values over time

  • Combine은 시간 경과에 따른 값 처리를 위한 통합 선언적 API입니다.
  • Combine은 기존의 API를 대체하지 않습니다.

Combine Features

  1. Generic
  2. Type Safe
  3. Composition first
  4. Request driven
  • Swift용으로 만들어져서 Generic과 같은 Swift 기능을 활용할 수 있습니다.
  • Type Safe 하므로 런타임이 아닌 컴파일 타임에 오류를 잡을 수 있습니다.
  • 여러 개를 구성하고 조합하여 쉽고 편리하게 사용할 수 있습니다.
  • 요청 기반이므로 앱의 메모리 사용량과 성능을 잘 관리할 수 있습니다.

Combine의 핵심 개념

  1. Publisher
  2. Subscriber
  3. Operators

Publishers

  1. Defines how values and errors are produced

    Combine API의 선언적 부분입니다. Publisher는 값과 오류가 생성되는 방식을 정의하는데요. 실제로 반드시 그것들을 생성하는 것은 아닙니다. 생성할 수도 있고, 아닐 수도 있다는 것이죠.

  2. Value type

    구조체를 사용하기 때문에 값(Value) 타입입니다.

  3. Allows registration of a Subscriber

    구독자(Subscriber) 등록도 허용합니다.

protocol Publisher {
	associatedtype Output
	associatedtype Failure: Error

	func subscribe<S: Subscriber>(_ subscriber: S)
		where S.Input == Output, S.Failure == Failure
}
  • Output

    생성하는 값의 종류입니다.

  • Failure

    생성하는 오류의 종류입니다. 에러를 생성할 수 없는 경우 Never 타입(associated type)을 이용할 수 있습니다.

  • 핵심 기능 (Subscribe: 구독)

    Subscriber의 입력은 Publisher의 출력과 일치해야 하고, Subscriber의 실패는 Publisher의 입력과 일치해야 합니다.

NotificationCenter

extension NotificationCenter {
	struct Publisher: Combine.Publisher {
		typealias Output = Notification
		typealias Failure = Never
		init(center: NotificationCenter, name: NotificationCenter.Name, object: Any? = nil)
	}
}

다음 코드는 NotificationCenter를 위한 Publisher입니다. Output은 Notification, Failure는 Never 타입입니다. 그리고 center, name, object 세 가지 항목으로 초기화 됩니다. 이것은 NotificationCenter를 대체하는 것이 아닙니다. 단지 적용하고 있는 것이죠.

Subscribers

  1. Receives values and a completion

    Subscriber는 Publisher의 반대되는 개념입니다. Publisher를 구독하고 Publiser가 유한(finite)한 경우 완료를 포함한 값을 받습니다.

  2. Reference type

    Subscriber는 일반적으로 값을 받으면 행동(처리)하고 상태를 변경하기 때문에 class와 동일하게 참조 타입을 사용합니다.

protocol Subscriber {
	associatedtype Input
	associatedtype Failure: Error

	func receive(subscription: Subscription)
	func receive(_ input: Input) -> Subscribers.Demand
	func receive(completion: Subscribers.Completion<Failure>)
}
  • Input

  • Failure

    Subscriber 가 실패를 수신할 수 없는 경우 Never 타입(associated type)을 이용할 수 있습니다.

  • 핵심 기능 (Receive: 구독을 받는 것)

    Receive는 Publiser에서 Subsriber로 데이터 흐름을 제어하는 방법입니다.

Assign

extension Subscribers {
	class Assign<Root, Input>: Subscriber, Cancellable {
		typealias Failure = Never
		init(object: Root, keyPath: ReferenceWritableKeyPath<Root, Input>)
	}
}

Assign 클래스는 Input을 받으면 해당 객체의 프로퍼티에 기록을 합니다. Swift에서는 프로퍼티 값을 작성할 때 오류를 처리할 방법이 없기 때문에 Assign의 Failure 유형은 Never로 설정합니다.

The Pattern

스크린샷 2022-08-09 오후 7 15 10

  1. 만약에 Subsriber를 가지고 있는 Controller 객체 또는 다른 타입이 있다고 가정해봅시다. 그리고 그것은 Subsciber와 함께 Publisher의 subsribe 함수를 호출해서 연결해주어야 하는 책임이 있습니다.
  2. 이 시점에서 Publisher는 Subsriber에게 Subscription을 보내, Subscriber가 Publisher에게 특정 수의 값 또는 무제한 요청을 할 수 있도록 합니다.
  3. Publisher는 해당 수 이하의 을 Subscriber에게 자유롭게 보낼 수 있습니다.
  4. Publisher는 유한한 경우 결국 Completion 또는 Error를 보냅니다.

스크린샷 2022-08-09 오후 7 32 09

졸업하는 학생들에 대한 알림을 듣고 졸업하면, Wizard 객체들의 값을 일괄적으로 업데이트 하고 싶습니다.

  1. 졸업에 대한 알림은 NotificationCenter의 Publisher를 이용해서 알립니다.
  2. Assign Subscriber를 만들고 merlin의 학년(grade)을 새로운 값으로 변경할 수 있도록 합니다.
  3. 그 이후에 구독(subscribe)을 통해 연결할 수 있는데요.

그런데 뭔가 예상대로 컴파일되지 않습니다. 그 이유는 타입이 일치하지 않기 때문입니다. 위에서 Publisher의 Output과 Subscriber의 Input은 타입이 일치해야한다고 했었죠? (기억안나면 어쩔 수 없구요…)

스크린샷 2022-08-09 오후 7 36 46

Notification Center에서 Notification을 보내지만 Assign이 Int 타입에 값을 쓰고 싶은 상황입니다. 우리는 그럼 지금 Int 타입이 필요한거죠. 그래서 이 사이에 변환하는 과정이 필요한데요. 이를 위해 필요한 것이 Operator입니다.

Operator

  1. Publisher 프로토콜을 채택합니다. 채택할 때까지는 Publisher인 것이죠.
  2. 선언적이므로 값 타입입니다.
  3. 값 변경, 추가, 제거 또는 다양한 종류의 동작을 수행합니다.
  4. upstream이라고 불리우는 다른 Publisher를 구독하고, 그 결과를 downstream이라고 부르는 Subscriber에게 결과를 보냅니다.

Map

extension Publishers {
	struct Map<Upstream: Publisher, Output>: Publisher {
		typealias Failure = Upstream.Failure
		
		let upstream: Upstream
		let transform: (Upstream.Output) -> Output
	}
}

Map이라는 Operator도 결국 Publisher였네요? Map은 어떤 Upstream에 연결하고 Upstream의 Output을 자체 Output으로 변환하는 방법으로 초기화되는 구조체입니다. 그리고 자체적으로 Failure를 생성하지 않습니다. 단순히 Upstream의 Failure 타입을 mirroring하고 그냥 전달합니다.

스크린샷 2022-08-09 오후 7 45 39
스크린샷 2022-08-09 오후 7 45 34

let converter = Publishers.Map(upstream: graduationPublisher) { note in 
	return note.userInfo?["NewGrade"] as? Int ?? 0
}

이 코드만 잠깐 이해하고 넘어가면 좋을 것 같아요. (참 흥미롭네요..ㅎ) 기존에 컴파일 에러가 났던 이유는 Publisher와 Subscriber의 타입이 맞지 않았기 때문입니다. 그렇기에 우리는 중간 과정이 필요했고, 그 역할을 도와주는 Operator가 등장한 것이죠. Operator 역시 결국 이전(upstream)의 값을 가지고 와서 변경하고 또 다른 Output을 내보내는 Publisher라고 봐도 될 것 같습니다.

자 다시 돌아와서 지금 우리는 Notification을 Int로 쓸 수 있게 하고 싶은 것이잖아요? graduationPublisher을 upstream으로 가져와서 처리를 해주면 됩니다. graduationPublisher의 Output인 Notification의 userInfo Int 값을 다시 반환해주면 될 것 같아요. 그러면 이제 정상적으로 subscribe가 가능해집니다.

extension Publisher {
	func map<T>(_ transform: @escaping (Output) -> T) -> Publishers.Map<Self, T> {
		return Publishers.Map(upstream: self, transform: transform)
	}
}

다음과 같이 구문을 좀 더 간단하게 사용할 수도 있습니다. 이렇게 extension을 이용하면 모든 Publisher가 사용할 수 있는 함수를 쓸 수 있게 되죠.

let cancellable = NotificationCenter.default.publisher(for: .graduated, object: merlin)
	.map { note in return note.userInfo?["NewGrade"] as? Int ?? 0  }
	.assign(to: \.grade, on: merlin)

이 구문을 보면 단계별로 매우 선형적이고 이해하기 쉬운 흐름을 제공하는 것을 알 수 있습니다. Assign은 cancelable 항목을 반환합니다. Cancelation 또한 Combine에 내장되어 있습니다. Cancelation을 통해 필요한 경우 Publisher 및 Subscriber를 조기에 해제할 수 있습니다.

이 단계별 구문은 Combine 사용 방법의 핵심입니다. 각 단계는 체인의 다음 명령어 세트를 설명합니다. 첫 번째 Publisher에서 일련의 Operator를 거쳐 Subscriber로 끝나는 값을 반환합니다. 그리고 이러한 Operator를 많이 가지고 있습니다. 우리는 이를 Declarative Operator API라고 부릅니다.

Declarative Operator API

  1. Functional transformations

    Map, Filter, Reduce 등이 있습니다.

  2. List operations

    Publisher의 첫 번째, 두 번째 또는 다섯 번째 element를 취하는 것과 같은 작업을 나열합니다.

  3. Error handling

    오류를 기본값 또는 대체값으로 바꾸는 것과 같은 작업도 가능합니다.

  4. Thread or queue management

    스레드, 큐에서 이동, 무거운 작업을 백그라운드 스레드 이동, UI 작업을 메인 스레드로 이동 From 루프, 디스패치 큐, 타이머 지원, 타임아웃 등과 같은 작업 역시 지원합니다.

  5. Scheduling and time

사실 Operator가 너무 많아서 연산자를 찾고 활용하는 방법에 대해 부담을 느낄 수 있습니다.

Try composition first

Apple에서 권장하는 것은 Combine에 대한 핵심 디자인 원칙으로 돌아가는 것입니다. 바로 Composition인데요. 애초에 설계부터 많은 작업을 수행하는 몇 가지 Operator를 제공하는 대신, 약간의 핵심 작업만 수행하는 Operator를 많이 제공하여 이해하기 쉽도록 구성했습니다. 개발자들이 쉽게 이해할 수 있도록 Swift Collection API에서 이름을 많이 따왔다고 합니다.

스크린샷 2022-08-09 오후 8 07 21

왼쪽은 동기 API이고, 오른쪽에는 비동기 API입니다. 위쪽은 단일 값, 아래쪽은 다수의 값을 다룹니다. 단일 값을 비동기적으로 나타내야 하는 경우 Future를 사용하고, 다수의 값을 비동기적으로 나타내야 하는 경우 Publisher를 사용하면 됩니다.

Operator Example

스크린샷 2022-08-09 오후 8 10 55

이 예시는 키가 없거나 정수가 아닌 경우 0을 반환하고 있습니다. 잘못된 값이 저장되도록 하는 것보다 nil을 반환하도록 하는 것이 더 좋아보입니다.

compactMap

스크린샷 2022-08-09 오후 8 10 59

이 때, Swift 4.1에 도입된 compactMap을 이용해볼 수 있습니다. compactMap도 역시 Combine에서 사용할 수 있습니다. 이러면 이전과 다르게 클로저에서 nil을 반환하면 compactMap은 이를 필터링하여 downstream으로 더 이상 진행되지 않도록 합니다.

filter

스크린샷 2022-08-09 오후 8 11 03

이번에는 filter operator를 이용해서 5학년 이상만 걸러지도록 해보겠습니다. Array에서 사용되던 filter와 동작이 일치합니다.

prefix

스크린샷 2022-08-09 오후 8 11 09

또는 반복되는 작업 동안 3번까지만 값을 전달하고 싶으면 prefix operator를 사용할 수도 있습니다.

Combining Publishers

2가지 Operator를 더 소개하겠습니다.

Zip

스크린샷 2022-08-09 오후 8 18 21

마법사 앱에서 지팡이를 생성하는 단계입니다. 지팡이를 만들기 위해서는 3가지 오래 걸리는 비동기 작업이 완료되어야 합니다. 사용자가 계속하도록 허용하려면 위의 작업이 완료되어야 하겠죠. zip을 이용하면 3개의 작업이 모두 완료된 시점을 캐치해서 Continue 버튼을 활성화 시킬 수 있습니다.

스크린샷 2022-08-09 오후 8 20 01

Zip은 여러 Upstream을 단일 Tuple로 변환합니다. downstream으로 값을 전달하고 작업을 진행하려면 모든 upstream에서 입력(Input)이 필요합니다. 첫 번째 Publisher에서 A를 생성하고, 두 번째 Publisher에서 1을 생성하면 이제 Tuple을 만들고 해당 값을 Subscriber에게 downstream으로 보낼 수 있습니다.

스크린샷 2022-08-09 오후 8 23 23

이 앱에서는 Bool 타입을 제공하는 3개의 비동기 작업의 결과를 기다리는데 3개의 upstream이 필요한 Zip 버전을 사용합니다. Tuple을 단일 Bool로 매핑하고 여기에서 버튼의 isEnabled 속성을 바꿔줍니다. 3개의 작업이 모두 완료되면 true값이 세팅되고 버튼이 활성화 됩니다.

CombineLatest

스크린샷 2022-08-09 오후 8 26 52

Play 버튼이 활성화되기 전에 3개의 스위치를 모두 활성화해야 하고, 하나라도 비활성화된다면 Play 버튼도 비활성화해야 합니다. 이럴 때 사용할 수 있는 것이 CombineLatest입니다.

스크린샷 2022-08-09 오후 8 26 23

Zip과 마찬가지로 여러 upstream의 Input을 단일 값으로 변환합니다. 각 upstream에서 받은 마지막 값을 저장하고 이를 단일 downstream 값으로 변환합니다. 첫 번째 Publisher가 A를 생성하고 두 번째 Publisher가 A1을 생성하면 이를 문자열화하고 downstream으로 내려보내고 나중에 두 번째 Publisher가 새 값을 생성하면 첫 번째 Publisher의 이전 값과 결합하여 새 값을 보냅니다.

upstream이 변경되면 새로운 이벤트가 발생합니다.

스크린샷 2022-08-09 오후 8 26 46
스크린샷 2022-08-09 오후 8 32 11

Combine을 사용하기 위해서 모든 것을 변환할 필요는 없습니다. 몇 가지부터 시작해보세요.

  1. NotificationCenter의 알림을 수신하고 조치 여부를 결정하려면 filter를 이용해보세요.
  2. 여러 비동기 작업의 결과에 가중치를 부여하면 네트워크 작업을 포함하여 Zip을 사용할 수 있습니다.
  3. URLSession을 사용하여 데이터를 받아와 JSON Decoder를 사용하여 해당 데이터를 특정 객체로 변환하는 경우에도 도움이 됩니다. decode operator를 확인해보세요.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
WWDC19 wwdc2019
Projects
None yet
Development

No branches or pull requests

1 participant