Combine 을 정리해보았습니다. # 기초편

Newon·2023년 9월 13일
0
post-thumbnail

이 글은 독자가 Generic Type, URLSession, 클로져와 Callback,
UIKit 혹은 SwiftUI 의 View LifeCycle 에 대해서
이미 알고 있음을 전제하고 있습니다.

Combine 이란?

Combine 은 앱 내에서 일어나는 이벤트들의 진행 경과들을 선언적으로 코딩할 수 있게끔 도와주는 Apple 의 프레임워크입니다.

어떠한 이벤트를 추적할 때 Delegate 패턴 을 사용하거나, Completion 클로져 를 사용하는 대신 Combine 을 활용해볼 수 있습니다.

이 프레임워크는 Swift API 에 해당하기에
Swift 언어를 사용한다면 UIKit 이나 SwiftUI 둘 다 사용할 수 있습니다.

더 쉽게 말하자면
URLSession.shared.dataTask { } 쓰면 completion() 써야하고
모시깽이 클래스 - 모시깽이 Delegate 를 적용하는 상황들이

직관적이지 않고, 조금만 복잡해져도 스파게티 코드가 되니
이걸 간단하게 해결해보자

라는 취지


Combine 개요

Combine 은 결국
Publisher 라는 이벤트 반응 전송기계
Subscriber 라는 이벤트 수집기계

이 두개를 연결해주는 프레임워크입니다.


SubscriberPublisher 에게 데이터를 받기만 하는 일방향적 관계이며,
SubscriberPublisher 에게 요청할 수 있는것은 데이터를 달라는 요청만 할 수 있습니다.

PublisherSubscriber 에게 데이터를 전달할 때, 바로 전달할 수도 있지만
Operator 를 활용해서, 데이터를 가공해서 줄 수 있습니다.

Operator 는 Swift 에서 일상적으로 쓰는 메소드들인
map, flatMap, compactMap, filter 등의 이름을 따서 만든 메소드들로, 이름과 유사한 기능들을 제공합니다.

더 쉽게 말하자면
Publisher 는 데이터를 전송만 담당,
Subscriber 는 데이터 수신만 담당
OperatorPublisher 가 데이터 전송할 때, 중간에 수정하는 역할

Subscriber 와 Publisher 의 타입

PublisherSubscriber
각각 실제 데이터에 해당하는 OutputInput,
각각 오류에 해당하는 FailureFailure 를 갖고 있습니다.

서로 연결되어있는 PublisherSubscriber
OutPutInput 이 같아야하며, 같은 Failure 를 갖고 있어야 합니다.

더 쉽게 말하자면
PublisherSubscriber 는 같은 타입이여야해용


Publisher 와 Subscriber 연결하는 방법

연결은 PubliserSubscriber 를 붙이는 형태로 동작합니다.

SomePublisher
	.sink(receiverCompletion:receiveValue:)`
    .assign(to:on:)
    
/*
 Publisher 에 sink() 혹은 assign() 을 사용하면
 새로운 Subscriber 를 만들 수 있습니다.
*/


Subscriber 는 새로운 Subscriber 를 생성할 수 있습니다.

SomeSubscriber
    .sink(receiverCompletion:receiveValue:)`
    .assign(to:on:)
    
/*

Subscriber 에 또 sink, 혹은 assign 을 붙여서
새로운 subscriber 를 선언할 수 있다.

*/

더 쉽게 말하자면
Publishersink() 혹은 assign() 쓰면
새로운 Subscriber 를 선언할 수 있어요.

Class 에는 Observable 프로토콜 (옛날 이름 BindableObject) 을 사용할 수 있는데,
Class 내부 변수에 @Published 만 붙이면, 자동으로 Publisher 로 만들어요.
@Published var posts = [T]()


Operators

OperatorPublisher 에 붙여서 사용할 수 있는 메소드입니다.
Publisher 가 뿜어주는 데이터들을 원하는대로 가공할 때 사용합니다.

let sub = NotificationCenter.default
    .publisher(for: NSControl.textDidChangeNotification, object: filterField)
    .map( { ($0.object as! NSTextField).stringValue } )
    .sink(receiveCompletion: { print ($0) },
          receiveValue: { print ($0) })
          
/*

TextField 의 변화를 감지하는 NotificationCenter 를 만들어서, 
해당 이벤트 (입력 감지) 기반으로 Publisher 를 만들고,
Publisher 가 뿜는 이벤트 기반 데이터 중, 실제 입력값인 String 값만 받도록 Map 해서
sink 를 통해 Subscriber 를 만든 후, 받은 이벤트 상태 (receiveCompletion) 과 값 (receiveValue) 를 출력하는 중 입니다.

*/

Operator 에는 다양한 종류가 있어, 필요에 따라 사용할 수 있습니다.
자주 사용될법한 Operator 들은 다음과 같이 있습니다.

	.map { }
    .flatMap { } // 이벤트가 1회 요청에 여러번 돌아올 때, 해당 값들을 모두 한번에 받습니다. (URLRequest 를 사용하는 시나리오가 대표적)
    .compactMap { } // nil 값은 제외하고 이벤트 값들을 방출합니다.
    .tryMap { } // 에러를 발생시키는 작업을 할 때, 에러가 발생하면 바로 Publisher 를 종료시킵니다.
    
    .filter { } // 이벤트 값들을 특정 조건에 따라 거릅니다.
    .tryFilter { } // 이벤트 값들을 특정 조건에 따라 거를 때, 에러가 발생하면 바로 Publisher 를 종료시킵니다.
    
    .setFailureType(to: Error.self) // 해당 에러를 Publisher 가 방출하는 Error 로 바꿉니다. 
    
    .decode(type: T.self, decoder: JSONDecoder()) // JSON 등의 데이터를 T 자료형으로 디코딩합니다. 실패하면 Publisher 를 종료시킵니다.
    
    .retry() // Publisher 의 흐름에 따라 내려올 때, 오류가 발생하면 retry의 횟수만큼 처음부터 다시 시도합니다. (URLRequest 를 실패했을 때, 1번 더 시도 하게 하는 등으로 사용할 수 있습니다.)
    .debounce(for: .milliseconds(500), scheduler: RunLoop.main) // 얼마나 자주 이벤트를 요청할 건지 debounce 를 통해 지정할 수 있습니다.
    
    .eraseToAnyPublisher() // Publisher 의 Operator 들을 거치며 생성된 중첩 상태의 OutPut 들 중 가장 마지막 타입을 기반으로 Publisher 로 1개의 타입으로 만듭니다.
    

Cancellabel

Subscriber 를 한번 만들면, 특정 이유에 의해서 종료되지 않는 이상 메모리에 계속해서 남아서 이벤트를 요청하게 됩니다. 이러한 Publisher 들은 필요 없을 때 해제해주지 않으면 메모리 누수로 이어지기 때문에 적절한 시점에서 해제하는 것이 필요합니다.

해제를 하는 방법에는 두가지 방법이 있습니다.

  • Subscriber.cancel() 을 호출해서 직접 해제하는 방법
  • Set<AnyCancellabel>() 에 Publisher 들을 모아서 해제하는 방법

Subscriber.cancel()

let sub = NotificationCenter.default
    .publisher(for: NSControl.textDidChangeNotification, object: filterField)
    .map( { ($0.object as! NSTextField).stringValue } )
    .filter( { $0.unicodeScalars.allSatisfy({CharacterSet.alphanumerics.contains($0)}) } )
    .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
    .receive(on: RunLoop.main)
    .assign(to:\MyViewModel.filterString, on: myViewModel)
    // 어떤 Subscriber
    
sub?.cancel() // << 원하는 시점에서 Cancel()

Publisher 에서 sink() 혹은 assign() 을 통해 만들어진 Subscriber 들을, 원하는 시점에서 직접 Cancel() 을 호출해서 메모리에서 해제할 수 있습니다.

대표적인 시점은
UIKit 에서는 viewDidDisapeear()
SwiftUI 에서는 OnDisAppear() 가 될 수 있습니다.

Set<AnyCancellabel>()

Subscriber 들이 다수 존재하고, 특이점이 없는 LifeCycle 을 가진다면 (즉, View가 사라질 때 함께 사라지면 된다면) Set<AnyCancellabel> 를 사용할 수 있습니다.

private var cancellabels = Set<AnyCancellable>() // Subscriber 들을 저장해놓은 Set


let sub = NotificationCenter.default
    .publisher(for: NSControl.textDidChangeNotification, object: filterField)
    .map( { ($0.object as! NSTextField).stringValue } )
    .filter( { $0.unicodeScalars.allSatisfy({CharacterSet.alphanumerics.contains($0)}) } )
    .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
    .receive(on: RunLoop.main)
    .assign(to:\MyViewModel.filterString, on: myViewModel)
    .store(in: &cancellabels) // << Subscriber 의 마지막에 store() 를 통해 해당 Subscriber 를 Set<Cancellabel> 에 저장한다.
    

// 이후 Cancellabels 가 사라질 때, 
// Cancellabels 에 저장된 Subscriber 들도 모두 함께 메모리에서 해제됩니다.

더 쉽게 말하자면
Subscriber 는 한번 만들면 메모리 해제해줘야해요.
Subscriber.cancel() 쓰거나,
Set<AnyCancellabel> - Subscriber.store() 를 통해
메모리에서 해제할 수 있어요.

읽어주셔서 감사합니당 :)

출처

Combine from Apple
Combine in Practice from Apple
URLSession dataTask Vs Combine dataTaskPublisher from Medium by PTeng

profile
나만 고양이 없어

0개의 댓글