본문 바로가기

Combine

[WWDC 19] Introducing Combine

 

https://developer.apple.com/videos/play/wwdc2019/722/

 

Introducing Combine - WWDC19 - Videos - Apple Developer

Combine is a unified declarative framework for processing values over time. Learn how it can simplify asynchronous code like networking,...

developer.apple.com

간단한 앱

  • 유저의 이름과 패스워드를 확인하는 과정이 있다.
    • 이러한 과정은 메인 스레드를 차단하지 않고 사용자의 입력과 반응이 호환되어야 한다.
  • 이미 많은 비동기 작업들이 일어 나고 있다.
    • Target / Action
      • 사용자의 입력에 대한 알림을 수신한다.
    • Timer
      • 사용자가 입력을 멈출 때까지 기다린다. → 서버에 네트워크 요청을 하지 않는다.
    • KVO ( Key - Value Observing)
      • 비동기 작업에 대한 진행률 업데이트

 

 

  • 입력을 하고나서 더 많은 비동기 작업을 수행한다.
    • URLSession에 대한 요청 응답 대기
    • 결과를 동기 검사 결과와 병합해야 한다. ( 비밀번호 )
    • 모든 작업이 완료 되면 KVC( Key - Value Coding)와 같은 것을 사용하여 UI업데이트

 

 

 

  • 비동기에 많은 요소들이 있는데 이것들을 같이 사용하는 경우 어려움을 느낄 수 있다.
  • 그렇기 때문에 공통점을 찾게 되었다.
  • 그래서 swift 전용 combine이 나오게 되었다.

 

 

Combine

  • swift용으로 나왔기 때문에 Generic을 사용가능하다
  • Combine은 type safe하다.
type safe???
타입에 안전하다는 의미, Int로 선언을 한 변수면 Int값만 들어가고 다른 값을 작성하는 경우 컴파일 타임에 오류가 발생한다.
  • compostion First??
    • 작은 연산자들을 결합하여 다양하게 만드는 것
  • combine은 요청 기반이므로 앱의 메모리 사용량과 성능을 주의 깊게 관리가 가능해진다.

 

 

combine 구성요소

  • Publishers: 값을 생산하고 호출
  • Operators: 값을 변경하는 로직
  • Subscribers: 값을 받기

 

Publishers

  • publisher는 struct를 사용
  • 여러 이벤트를 Subscriber에게 전송한다.
    • value
    • Successful Compltion
    • Failure
protocol Publisher {
		associatedtype Output
		associatedtype Failure: Error
		
		func subscribe<S: Subscriber>(_ subscriber: S)
		where S.Input == Output, S.Failure == Failure
}
  • Subscriber의 Input과 Publisher의 output이 동일
  • Subscriber의 Failure과 Publisher의 Failure이 동일

 

Publisher의 예시

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

 

 

 

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>)
}
  • 3가지의 주요 기능이 존재
    • 구독을 통해 publisher에서 전해준 데이터의 흐름을 제어하는 방법
    • 입력 받기
    • Publisher가 종료 되면 종료에 대한 이벤트 결과를 받을 수 있다

 

Subscription: publisher와 subscriber의 연결을 나타내는 프로토콜

 

Subscriber 예시

extension Subscribers {
	class Assign<Root, Input>: Subscriber, Cancellable {
		typealias Failure = Never
		init(object: Root, keyPath: ReferenceWritableKeyPath<Root, Input>)
	}
}
  • swift에서 속성 값만 작성하는 경우 오류를 처리할 방법이 없기 때문에 Failure를 Never로 설정

 

사용예시

 

  • subscriber를 할당
  • publisher가 subscriber에게 subscription을 보냄
  • demand를 통해 특정 개수 또는 무제한의 값 요청이 가능
  • publisher가 해당 개수 이하의 값을 자유롭게 보냄
  • publisher가 유한한 경우 완료 또는 실패를 보냄

one Subscription, zero or more values and a single Completion

1개의 구독, 0개 또는 많은 값과 단일 compleiton

  • 학생들의 졸업에 대한 알림을 수신하고 싶다.

 

// Using Publisher and Subscriber
class Wizard {
    var grade: Int
}
let merlin = Wizard(grade: 5)

let graduationPublisher =
NotificationCenter.Publisher(center: .default, name: .graduated, object: merlin)

let gradeSubscriber = Subscribers.Assign(object: merlin, keyPath: \.grade)

graduationPublisher.subscribe(gradeSubscriber)
  • 다음과 같이 작성을 하면 컴파일이 안된다.
  • 타입이 다르기 때문

 

  • 중간에 과정을 통해 변환을 시켜주어야 한다. 이럴 때 사용하는게 Operator다.

 

 

Operator

  • value Type이다
  • Declarative Operator API ( 선언적 연산자 API )
    • 많은 기능들이 존재한다.

 

업 스트림 (Upstream): publisher를 구독하는 것
다운 스트림 (Downstrem): subscriber에게 보내는 것

 

 

예시

extension Publishers {
    struct Map<Upstream: Publisher, Output>: Publisher {
        typealias Failure = Upstream.Failure
        
        let upstream: Upstream
        let transform: (Upstream.Output) -> Output
    }
}
  • 연결되는 업스트림과 업스트림의 출력을 자체 출력으로 변환하는 방법으로 초기화되는 구조체
  • Map은 자체적으로 Failure를 생성하지 않기 때문에 업스트림의 Failure 유형을 통과 시킨다.

 

Map을 통해 직전에 컴파일이 불가능하던 코드를 변화 시켜보자

let graduationPublisher =
NotificationCenter.Publisher(center: .default, name: .graduated, object: merlin)

let gradeSubscriber = Subscribers.Assign(object: merlin, keyPath: \.grade)

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

converter.subscribe(gradeSubscriber)
  • GraduationPublisher를 연결하고 해당 userInfo에 “NewGrade”가 정수로 변환이 되면 해당 정수를 반환하고 아니면 0을 반환한다.

 

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

모든 publisher가 사용이 가능하도록 프로토콜에 해당 기능 추가

 

 

실제 사용 예시를 보자

// Chained Publishers
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은 취소 기능도 결합이 되어 있다.
  • 취소를 사용하면 필요한 경우 게시자 및 구독자의 순서를 조기에 중단하는 것이 가능하다

 

Combine을 잘 사용하는 방법

  • Compostion first이 기본 원칙이다.
    • 작은 연산자들을 결합 하는 것

  • 많은 정수를 동기로 표현하려면 정수 배열과 같은 것을 사용한다.
  • 이러한 개념을 가져와 비동기 세계에 매핑했다
  • 단일 값을 비동기로 표현해야 하는 경우, 나중에 발생하기 때문에 미래를 가지고 있다.
  • 많은 값을 비동기적으로 표현해야 하는 경우는 Publisher다

 

// Chained Publishers
let cancellable =
NotificationCenter.default.publisher(for: .graduated, object: merlin)
    .map { note in
        return note.userInfo?["NewGrade"] as? Int ?? 0
    }
    .assign(to: \.grade, on: merlin)
  • 기존의 코드는 잘못된 값이 그대로 저장이 되는데 이것을 방지해보자

 

let cancellable =
NotificationCenter.default.publisher(for: .graduated, object: merlin)
    .compactMap { note in
        return note.userInfo?["NewGrade"] as? Int
    }
    .assign(to: \.grade, on: merlin)
  • compactMap의 클로저에서 nil을 반환하면 이를 필터링하여 스트림 아래로 더 이상 진행이 안된다.

 

// Composing Operators
let cancellable =
NotificationCenter.default.publisher(for: .graduated, object: merlin)
    .compactMap { note in
		    return note.userInfo?["NewGrade"] as? Int
    }
    .filter { $0 >= 5 }
    .prefix(3)
    .assign(to: \.grade, on: merlin)
  • int로 변환이 가능하고, 5학년이상이며, 3번의 졸업만 가능하도록

map과 filter모두 좋은 연산자이지만 주로 동기적 상황에서 사용한다.

 

 

 

Zip

 

 

  • zip은 업스트림의 입력을 단일 튜플로 변환한다.
  • 그렇기 때문에 3가지 비동기 작업의 결과를 기다리기 위해 zip을 사용한다.
Zip3(organizing, decomposing, arranging)
	.map { $0 && $1 && $2 }
	.assign(to: \.isEnabled, on: continueButton)

 

 

Combine Latest

  • zip과 마찬가지로 여러 upstream을 단일로 변경한다.
  • 하지만 zip과 달리 진행하려면 업스트림의 입력이 필요해 일종의 when/or 작업이 된다.
  • 업스트림이 변경이 되면 새로운 이벤트가 발생

  • 그렇기 때문에 3개의 업스트림을 단일 bool로 변경하여 이를 isEnabled에 기록한다.
  • 1개라도 거짓이면 거짓이다

 

 

combine 사용예시

  • 알림 센터 사용하고 알림을 받은 다음, 내용을 살펴보고 조치를 취할지 여부를 결정하는 경우 filter를 사용
  • 여러 비동기 작업의 결과에 가중치를 부여하는 경우 네트워크 작업을 포함하여 Zip을 사용
  • URLSession을 사용하여 데이터를 받아온 경우 decoding할 때 decode라는 연산자

'Combine' 카테고리의 다른 글

[WWDC 19] Combine in Practice  (0) 2024.07.08