[Swift/Combine] 이벤트 생성 및 수신

이정훈·2023년 2월 18일
0

Combine Framework

목록 보기
2/4
post-thumbnail

이전 포스트에서 publisher의 개념과 subscriber에 대한 개념을 알아 보았다.

다시 간략히 정리하자면,

  • subscriber는 observer 객체로 이벤트(값)을 수신
  • publisher는 observable 객체로 이벤트 발생을 감지하고 subscriber로 이벤트(값)를 전달한다.

이번 포스트에서는 사용자 정의 이벤트를 생성하고 수신하는 방법에 대하여 알아보고자 한다.

또한 Foundation framework에 정의 되어 있는 여러 타입 중에서 Timer, NotificationCenter, URLSession 등을 통해 publisher를 생성할 수 있는데 이번에는 NotificationCenter를 통해 알아 보고자 한다.


NotificationCenter

시작에 앞서 NotificationCenter class에 대하여 알아보려고 한다.
Apple 문서에는 다음과 같이 설명 되어 있다.

NotificationCenter

A notification dispatch mechanism that enables the broadcast of information to registered observers.

NotificationCenter는 class 타입으로 observer에게 정보를 전달할 수 있도록 notification을 전달하는 매커니즘 정도로 보면 될듯 하다.


NotificationCenter로 Publisher 생성

NotificationCenter와 Combine을 사용하면 다음과 같이 Publisher를 만들 수 있다.

import Combine
import Foundation

let notification: Notification.Name = Notification.Name("pass Notification")
let publisher: NotificationCenter.Publisher = NotificationCenter.default.publisher(for: notification)

❗️ NotificationCenter와 Notification은 모두 Foundation framework에 정의 되어 있으므로 Foundation을 import 하도록 한다.

먼저 전달할 notification을 식별할 수 있도록 NotificationName을 통해 NotificationCenter에게 전달할 notification을 정의 하였다.

그리고 NofiticationCenterdefaultpublisher 프로퍼티로 notification을 등록하면 해당 notification이 NotificationCenter로 들어 왔을때 값을 observer에게 전달하는 publisher가 생성된다.


post 메서드

여기서 publisher와 subscriber 데이터 전달의 핵심은 바로 NotificationCenter의 post 메서드이다.

NofiticationCenterdefaultpost 메서드를 호출하면 publisher에게 매개변수로 전달된 notification을 전달한다.

그럼 publisher는 이벤트 발생을 감지하고 observer에게 이벤트(값)을 전달한다.

NotificationCenter.default.post(Notification(name: notification))

하지만 아직 subscriber가 publisher를 subscribe하지 않았기 때문에 post 메서드를 호출해 봤자 아무 일도 일어나지 않는다.


Publisher subscribe

이전 포스트와 마찬가지로 Combine의 sink 메서드를 통해 publisher를 subscribe 해보려고 한다.

let subscribtion: AnyCancellable = publisher.sink(receiveCompletion: {
    print("received completion: \($0)")
}, receiveValue: {
    print("received value: \($0)")
})

이제 publisher까지 subscribe 했기 때문에 post 메서드를 호출하면 NotificationCenter에 notification을 전달한다. 그럼 publisher가 이벤트 발생을 감지하고 observer에게 이벤트(값)을 전달하게 된다.

NotificationCenter.default.post(Notification(name: notification))
NotificationCenter.default.post(Notification(name: notification))

post 메서드는 한번 호출되면 한번의 notification을 전달하기 때문에 여러번 호출하면 여러번의 notification이 전달되어 다음과 같은 결과나 나온다.

//출력 결과
//received value: name = pass Notification, object = nil, userInfo = nil
//received value: name = pass Notification, object = nil, userInfo = nil

결과 값을 보면 알겠지만 Publisher가 receiveValue parameter로 값을 전달한 것은 알 수 있으나 receiveCompletion parameter로는 completion이 전달 되지 않을 것을 알 수 있다.

그 말인 즉, publisher는 프로그램이 종료 되기 전까지 observer에게 값을 전달할 준비를 계속 하고 있기 때문에 더 이상 전달할 값이 없다면 AnyCancellable 타입의 cancle() 메서드를 사용하면 더 이상 publisher가 observer에게 값을 더 이상 전달하지 않도록 한다.

subscribtion.cancel()

당연하게도 cancel() 메서드를 사용하면 이후로는 post 메서드를 호출하여도 아무 일도 일어나지 않는다.

혹은 두번째 방법으로 AnyCancellable 타입의 store 메서드를 사용하는 방법이 있다.

AnyCancellable 타입을 가지는 Set을 생성하고 store 메서드로 위의 subscrition을 넘겨주면 알아서 subscribe를 해제할 수 있다.

var subsribtionSet = Set<AnyCancellable>()    //subscribe 해제를 위한 set

subscribtion.store(in: &subsribtionSet)   //inout 매개변수로 전달

먼저 AnyCancellable 타입을 담을 Set을 정의하고 해당 Set을 AnyCancellable 타입의 store 메서드로 전달하면 된다.


KVO (Key-Value Observing)

또 다른 이벤트를 감지하는 방법으로 KVO, 다시 말해 Key-Value Observing 방식으로 인스턴스 프로퍼티(Key)의 값(Vaue)을 감시하고 변화를 관찰할 수 있다.

먼저 클래스와 관찰하기 위한 프로퍼티를 정의한다.

import Combine
import Foundation

//KVO: Key-Value-Observing
class Person {
    var age: Int {
        didSet {
            print("changed value: \(age)")
        }
    }
    
    init(_ age: Int) {
        self.age = age
    }
}

다음으로 인스턴스를 생성하고 프로퍼티의 값을 변경 했을때, 제대로 변화를 감지하는지 확인한다.

var personLee: Person = Person(99)
//assign으로 publisher로 부터 전달 받은 값을 바로 변경
let cancellable: AnyCancellable = [20].publisher.assign(to: \.age, on: personLee)    //to: 변경할 프로퍼티, on: 대상 인스턴스

이전 포스트에서 언급한 것과 같이 publisher를 subscribe하는 방법에는 두 가지 방법이 있는데 하나는 sink 메서드이고 또 하나는 위에서 사용한 assgin 메서드이다.

assign 메서드는 매개변수로 대상이 되는 프로퍼티를 KeyPath로 전달하고 해당 프로퍼티가 존재하는 인스턴스도 함께 전달한다.

그럼 publisher가 값을 보내면 전달 받은 값을 해당 프로퍼티의 값으로 바로 변경 가능하다.

그리고 프로퍼티의 값을 변경 했을때 결과 값은 다음과 같다.

//실행 결과
//changed value 20

@Published


또 다른 방법으로 관찰하고자 하는 프로퍼티를 @Published attribute를 사용하여 정의하면 해당 프로퍼티를 대상으로 하는 publisher를 생성한다.

@Published attribute는 class의 var 프로퍼티에만 사용 가능하니 주의

import Combine
import Foundation

class Person {
    @Published var age: Int
    
    init(_ age: Int) {
        self.age = age
    }
}

인스턴스를 생성 후 publisher를 가져오기 위해 인스턴스의 프로퍼티를 binding 즉,$ prefix를 통해 publisher를 가져온다.

이때 프로퍼티가 Int 타입으로 반환되는 publisher의 타입은 Published<Int>.Publisher

var personLee: Person = Person(99)
let publisher: Published<Int>.Publisher = personLee.$age
let subscriber: AnyCancellable = publisher.sink(receiveValue: {
    print("value changed: \($0)")
})

personLee.age = 20

프로퍼티의 값을 변경 했을때 결과는 다음과 같다.

//실행 결과
//value changed: 99    //초기값
//value changed: 20

정리


필자는 위의 KVO 방식을 구현하면서 반응형 프로그래밍 방식을 이해하는데 조금 더 도움이 되지 않았나 싶다.

감시하는 프로퍼티를 계속 지켜보고 있다가 값의 변화(이벤트)가 발생 하였을때 이벤트를 감지하고 publisher는 observer에게 값을 전달한다.

다음 포스트에서는 NotificationCenter와 KVO를 활용한 실질적인 예제를 한번 다루어 보려고 한다.

profile
새롭게 알게된 것을 기록하는 공간

0개의 댓글