Combine

Tabber·2023년 8월 27일
0

Reactive Extension

목록 보기
4/4
post-thumbnail

Combine

컴바인은 Apple의 최대 개발자 컨퍼런스인 WWDC발표 중 WWDC 2019에 발표된 프레임워크 중 하나입니다.

Combine은 시간에 흐름에 따른 값을 처리하기 위한 프레임워크입니다. 시간의 흐름에 따른 값을 처리하기 위해서는 비동기적인 이벤트가 필요할 것입니다. 따라서 Combine은 Publisher 라는 개념으로 변경될 값을 뿌려줄 역할을 하고, 반대로 Subscriber 라는 개념으로 변경된 값을 받도록 선언합니다.

그리고 이 이벤트로 비동기적인 네트워킹 처리나, KVO 패턴, 콜백과 같은 처리등을 할 수 있습니다.

Combine은 왜 만들었어요?

방금 위에서 설명한 정의 문장들 중에 답이 있습니다.

Combine은 시간에 흐름에 따른 값을 처리하기 위한 프레임워크입니다.

코드의 흐름을 쉽고, 명확하게 판단하고 처리해야 할 필요성이 생겼기 때문에 Combine이 만들어졌습니다.

기존의 컴플리션 핸들러로 코드를 작성하고, 에러가 발생한 곳을 판단하라고 하면 머리가 어질어질해 집니다.

또한, 코드를 처음 본 상태로 컴플리션 핸들러를 따라가다 보면, 내가 어디에서 시작했고 어디가 끝인지 알 수 없는 미궁속으로 빠지죠.

물론 Combine을 사용하는 이유로 다른 것도 존재하겠지만, 저는 “코드의 흐름” 을 개발자가 쉽게 파악하고, 개발자가 원하는 대로 흐름을 짤 수 있다라는 점에서 Combine을 사용하는 이유는 이미 명확해졌다고 생각합니다.

자 그럼 Combine에서 사용하는 Publisher와 Subscriber 의 개념에 대해서 먼저 생각해볼까요?

Publisher

Publisher는 시간에 따른 값의 흐름을 Subscriber 에게 전달할 수 있는 “프로토콜” 이다.

Output Associated Type 을 통해 개발자가 원하는 값을 실어서 보낼 수 있고,
Failure Associated Type 을 통해서 에러상황시 발생하는 값을 실어서 보낼 수 있다.

Associated Type 은 추후 다시 살펴보자 :)

그리고 이렇게 전달되는 값은 receive 함수를 통해서 값을 전달받는다.

After this, the publisher can call the following methods on the subscriber:

이후에 Publisher는 Subscriber에게서 다음과 같은 함수들을 호출할 수 있다.

  • receive(subscription:) : Tells the subscriber that it has successfully subscribed to the publisher and may request items.
    Subscriber에게 “현재 구독중인 상태이며, 수신받을 데이터를 요청할 수 있다는 것을 알릴 수 있다.
  • receive(_:) : Tells the subscriber that the publisher has produced an element.
    Publisher 가 Subscriber에게 요소를 생성했음을 알려줄 수 있다.
  • receive(completion:) : Informs the subscriber that publishing has ended, either normally or with an error.
    Subscriber에게 Publishing이 끝났음을 알리는데, 정상적으로 종료됐거나 에러가 발생했다는 상황을 알려줄 수 있다.

Subscriber

Subscriber는 Publisher와 반대 개념으로 Publisher로부터 받을 수 있는 형식을 선언할 수 있는 프로토콜이다.

Subscriber의 Associated Type을 살펴보자.

Publisher와 반대 개념이기에 타입 자체도 Input으로 정의되어 있는 모습이다.

아래 정의된 함수들의 역할이 무엇인지 살펴보자.

  • func receive(subscription: Subscription) : Subscriber에게 성공적으로 구독을 진행하고 데이터를 수신받을 수 있는 상태임을 알릴 때 사용한다.
  • func receive(_ input: Self.Input) -> Subscribers.Demand : Subscriber가 초기 요청을 한 후 Publisher는 이 함수를 호출하여 새로 바뀐 Input 데이터를 전달한다.
  • receive(completion:) : Publisher가 Publishing이 완료됐을 때 호출된다.

다 각자 사용처가 있는 함수들이었다.

그럼 이 개념을 적용하여 Combine을 어디에 사용하면 좋을까?

Combine을 사용해보자

Combine의 Publisher, Subscriber 개념을 파악하기 위해서 Publisher의 예시로 testDummy에 존재하는 text를 출력하는 Publisher를 생성하여 돌려보았다.

receive 함수에는 testDummy를 생성하여 존재하는 단어들을 receive 시키고 마지막으로 finished를 receive시키는 코드를 구성하였다. 아까 위에서 Subscribe 프로토콜 내부에서 정의되어있는 receive사용하여 작업이 진행된 내용을 수신받을 수 있게 하는 코드로 생각하면 될 것 같다.

실제 사용은 아래 코드와 같이 sink로 보내주는 데이터를 수신받을 수 있다.

이렇게 receive내부에 따로 커스텀을 통해 원하는 액션을 수행시키게끔 할 수도 있고, 미리 정의된 작업들을 수행할 수 있게 해주는 다양한 Publisher들이 존재한다.

Future

현재의 시점이 아닌 미래에 일어날 액션에 대해 비동기적으로 생성하고 보낼 수 있는 Publisher

let futureAction = Future<Bool, Error> { promise in
    promise(.success(true))
}

let publisherTest = PublisherTest()

_ = futureAction
    .sink(receiveCompletion: { receive in
        switch receive {
        case .failure(_):
            print("Failure")
        case .finished:
            print("Finished")
        }
    }, receiveValue: { result in
        print(result)
    })

// true
// finished

Future의 클로저로 감싸진 액션은 비동기로 수행될 수 있고, 수행된 데이터의 값은 pomise라는 Result 리턴값으로 Publishing된다. API 통신이나, 계산이 오래 걸리는 작업을 수행할 때 유용하다.

Just

Subscribe에게 한번만 보내는 단일 이벤트 전송 Publisher

let justAcion = Just<Bool>(false)

_ = justAcion.sink(receiveCompletion: { receive in
    switch receive {
    case .failure(_):
        print("Failure")
    case .finished:
        print("Finished")
    }
}, receiveValue: { result in
    print(result)
})

// false
// Finished

false Boolean 값을 입력시킨 Just Publisher를 수신받기 위해 sink 하였고, false 값을 수신받고 finished 되는 코드이다.

이렇게 단일, 단순 이벤트를 전송시키는 상황이 있을 경우 Just Publisher를 사용하면 유용하다.

Defferd

Subscribe를 하는 시점에 생성되는 Publisher

class PublisherTest: Publisher {
    typealias Output = String
    typealias Failure = Never
    
    init() {
        print("PublisherTest init")
    }
    
    func receive<S>(subscriber: S) where S : Subscriber, Never == S.Failure, String == S.Input {
        
        let testDummy: [String] = ["Hello"]
        testDummy.forEach { text in
            _ = subscriber.receive(text)
        }
        
        subscriber.receive(completion: .finished)
    }
}

let defferdAction = Deferred<PublisherTest> { () -> PublisherTest in
    return PublisherTest()
}

print("defferd Init")

_ = defferdAction.sink(receiveCompletion: { receive in
    switch receive {
    case .failure(_):
        print("Failure")
    case .finished:
        print("Finished")
    }
}, receiveValue: { result in
    print(result)
})

// defferd Init
// PublihserTest init
// Hello
// Finished

쉽게 생각하면 Lazy와 같은 느낌이라고 보면 될 것 같다. Lazy도 메모리에 올라가는 시점이 실제 사용하거나 액션이 들어왔을 경우인 것과 비슷하게 생각하면, 실제 구독이 시작되는 시점에 Defferd가 Publisher를 생성하여 Subscribe 할 수 있게 해준다.

이외에 Empty, Fail, Record 등이 존재한다.

RxSwift 와는 같은거에요?

Rx가 뭔지부터 알아야 이 질문은 잘못된 질문이라고 생각할 수 있겠다.

Rx

Reactive eXtension의 줄임말

관찰 가능한 연속성(순차적) 형태와 함수형태의 연산자를 이용해 비동기 & 이벤트를 위한 코드로 구성되어있는 집합

저기서 설명하는 것에 모든 답이 존재한다.

Combine도 비동기 작업이지만 순서를 보장하기 위해 사용한다고 했었다. RxSwift 또한 같은 이치로 사용하고 있기 때문에 코드에서 표현하는 방식만 다르지, 실제 액션은 같다.

https://github.com/CombineCommunity/rxswift-to-combine-cheatsheet

하지만 약간의 차이는 존재한다.

기본적으로 스펙에서만 보더라도 iOS 최소버전에 차이가나고, UI Binding 또한 Rx는 UIKit베이스지만, Combine은 SwiftUI를 베이스로 제작되었기에 각 플랫폼에서 최적의 방식으로 코드를 구성하면 좋을 것 같다.


정리

오늘은 Combine이 어떤것인지 알아보고, 어디에서 사용하면 좋을지에 대해 생각해보는 글을 적어보았다.
각 Publisher 들은 각자의 유용한 역할들을 가지고있고, 적재적소에 사용하면 좋은 코드를 만들 수 있다고 생각한다.

RxSwift와는 같은 개념이기에 Publisher, Subscriber 의 개념을 알고 있다면 두개를 응용하며 사용하기에는 문제가 되지 않을 것이다.

profile
iOS 정복중인 Tabber 입니다.

0개의 댓글