-
Notifications
You must be signed in to change notification settings - Fork 0
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
Comments
Introducing CombineWWDC19에는 Apple의 최신 Framework인 Combine을 소개합니다. 비동기 프로그래밍에 대해서 이야기하자고 하면서 서막을 여는데요. 같이 예시를 보고 이해하면서 Combine에 대해서 알아보도록 합시다. 간단한 회원가입 화면을 예시로 듭니다. 이 화면에서의 요구사항은 크게 다음과 같습니다. 우선 사용자 이름이 유효한지 네트워크에 요청을 해서 확인을 합니다. 그리고 비밀번호가 서로 일치하는지 확인을 합니다. 이 모든 작업은 메인 스레드에서 차단되지 않고 반응형 인터페이스를 유지해야 합니다. 먼저 사용자 이름을 입력하기 시작합니다. 여기서도 이미 많은 비동기 작업이 진행되고 있습니다. Target/Action을 사용하여 사용자 입력에 대한 알림을 수신했습니다. 여기서 많은 네트워크 요청으로 서버를 overwhelm하지 않기 위해서 Timer를 사용하여 사용자가 입력을 잠시 멈출 때까지 기다립니다. 그리고 KVO와 같은 것을 사용해서 해당 비동기 작업에 대한 진행률 업데이트를 수신합니다. 이름과 암호를 입력하면서 서버와 통신하고 유효한 값인지를 체크해줍니다. 그리고 그에 따라 UI도 업데이트 해주게 됩니다. Asynchronous Interfaces
Cocoa SDK 전반에 걸쳐서 비동기 인터페이스는 정말 상당히 많습니다. 이 모든 것들은 각각 중요하고 각기 다른 사용법을 가지고 있습니다. 이것들을 함께 사용할 때 어려울 수 있습니다. 이 모든 것들을 대체하지 않고, 공통점을 찾기 위해 Combine이 등장하게 됩니다. CombineA unified, declarative API for processing values over time
Combine Features
Combine의 핵심 개념
Publishers
protocol Publisher {
associatedtype Output
associatedtype Failure: Error
func subscribe<S: Subscriber>(_ subscriber: S)
where S.Input == Output, S.Failure == Failure
}
NotificationCenterextension 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
protocol Subscriber {
associatedtype Input
associatedtype Failure: Error
func receive(subscription: Subscription)
func receive(_ input: Input) -> Subscribers.Demand
func receive(completion: Subscribers.Completion<Failure>)
}
Assignextension 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
졸업하는 학생들에 대한 알림을 듣고 졸업하면, Wizard 객체들의 값을 일괄적으로 업데이트 하고 싶습니다.
그런데 뭔가 예상대로 컴파일되지 않습니다. 그 이유는 타입이 일치하지 않기 때문입니다. 위에서 Publisher의 Output과 Subscriber의 Input은 타입이 일치해야한다고 했었죠? (기억안나면 어쩔 수 없구요…) Notification Center에서 Notification을 보내지만 Assign이 Int 타입에 값을 쓰고 싶은 상황입니다. 우리는 그럼 지금 Int 타입이 필요한거죠. 그래서 이 사이에 변환하는 과정이 필요한데요. 이를 위해 필요한 것이 Operator입니다. Operator
Mapextension 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하고 그냥 전달합니다. 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
사실 Operator가 너무 많아서 연산자를 찾고 활용하는 방법에 대해 부담을 느낄 수 있습니다. Try composition firstApple에서 권장하는 것은 Combine에 대한 핵심 디자인 원칙으로 돌아가는 것입니다. 바로 Composition인데요. 애초에 설계부터 많은 작업을 수행하는 몇 가지 Operator를 제공하는 대신, 약간의 핵심 작업만 수행하는 Operator를 많이 제공하여 이해하기 쉽도록 구성했습니다. 개발자들이 쉽게 이해할 수 있도록 Swift Collection API에서 이름을 많이 따왔다고 합니다. 왼쪽은 동기 API이고, 오른쪽에는 비동기 API입니다. 위쪽은 단일 값, 아래쪽은 다수의 값을 다룹니다. 단일 값을 비동기적으로 나타내야 하는 경우 Future를 사용하고, 다수의 값을 비동기적으로 나타내야 하는 경우 Publisher를 사용하면 됩니다. Operator Example이 예시는 키가 없거나 정수가 아닌 경우 0을 반환하고 있습니다. 잘못된 값이 저장되도록 하는 것보다 nil을 반환하도록 하는 것이 더 좋아보입니다. compactMap이 때, Swift 4.1에 도입된 compactMap을 이용해볼 수 있습니다. compactMap도 역시 Combine에서 사용할 수 있습니다. 이러면 이전과 다르게 클로저에서 nil을 반환하면 compactMap은 이를 필터링하여 downstream으로 더 이상 진행되지 않도록 합니다. filter이번에는 filter operator를 이용해서 5학년 이상만 걸러지도록 해보겠습니다. Array에서 사용되던 filter와 동작이 일치합니다. prefix또는 반복되는 작업 동안 3번까지만 값을 전달하고 싶으면 prefix operator를 사용할 수도 있습니다. Combining Publishers2가지 Operator를 더 소개하겠습니다. Zip마법사 앱에서 지팡이를 생성하는 단계입니다. 지팡이를 만들기 위해서는 3가지 오래 걸리는 비동기 작업이 완료되어야 합니다. 사용자가 계속하도록 허용하려면 위의 작업이 완료되어야 하겠죠. zip을 이용하면 3개의 작업이 모두 완료된 시점을 캐치해서 Continue 버튼을 활성화 시킬 수 있습니다. Zip은 여러 Upstream을 단일 Tuple로 변환합니다. downstream으로 값을 전달하고 작업을 진행하려면 모든 upstream에서 입력(Input)이 필요합니다. 첫 번째 Publisher에서 A를 생성하고, 두 번째 Publisher에서 1을 생성하면 이제 Tuple을 만들고 해당 값을 Subscriber에게 downstream으로 보낼 수 있습니다. 이 앱에서는 Bool 타입을 제공하는 3개의 비동기 작업의 결과를 기다리는데 3개의 upstream이 필요한 Zip 버전을 사용합니다. Tuple을 단일 Bool로 매핑하고 여기에서 버튼의 isEnabled 속성을 바꿔줍니다. 3개의 작업이 모두 완료되면 true값이 세팅되고 버튼이 활성화 됩니다. CombineLatestPlay 버튼이 활성화되기 전에 3개의 스위치를 모두 활성화해야 하고, 하나라도 비활성화된다면 Play 버튼도 비활성화해야 합니다. 이럴 때 사용할 수 있는 것이 CombineLatest입니다. Zip과 마찬가지로 여러 upstream의 Input을 단일 값으로 변환합니다. 각 upstream에서 받은 마지막 값을 저장하고 이를 단일 downstream 값으로 변환합니다. 첫 번째 Publisher가 A를 생성하고 두 번째 Publisher가 A1을 생성하면 이를 문자열화하고 downstream으로 내려보내고 나중에 두 번째 Publisher가 새 값을 생성하면 첫 번째 Publisher의 이전 값과 결합하여 새 값을 보냅니다. upstream이 변경되면 새로운 이벤트가 발생합니다. Combine을 사용하기 위해서 모든 것을 변환할 필요는 없습니다. 몇 가지부터 시작해보세요.
|
The text was updated successfully, but these errors were encountered: