본문 바로가기

🍎 iOS/Combine

[Combine] 관찰가능한 데이터 만들기 - ObservableObject와 @Published

안녕하세요 제인입니다 :)

SwiftUI관련 내용을 정리하다보니 Combine 관련 내용이 나와버려서 Combine으로 돌아왔습니닷..ㅎ

SwiftUI로 개발하면서 데이터를 다루다보면 무조건 Combine을 공부할 수밖에 없는 것 같아요!

오늘은 관찰가능한 데이터를 만들어서 데이터 변경에 따라 UI를 어떻게 업데이트할 수 있는지에 대해 한번 다뤄보겠습니다.

@Published

@propertyWrapper
struct Published<Value>

 

  • Publisher로 만들어주는 프로퍼티 래퍼
  • 값 바뀔 때마다 willSet 블록에서 publish 
  • $ 기호를 이용하여 projectedValue에 접근
  • 클래스의 프로퍼티에서만 사용 가능 (값 타입 불가)

저번 Combine 게시글에서 Publisher와 Subscriber가 무엇인지 개념적인 부분에 대해 정리했었는데요,

그렇다면 어떻게 Publisher를 만들고 이것을 구독하냐?

Combine에서는 아주 쉽게 Publisher를 만들 수 있도록 해주는 프로퍼티 래퍼를 제공해줍니다.

예제 코드

import Foundation
import Combine

class Weather {
    @Published var temperature: Double
    init(temperature: Double) {
        self.temperature = temperature
    }
}


let weather = Weather(temperature: 20)
_ = weather.$temperature
    .sink() {
        print("\(weather.temperature) will set")
        print ("Temperature now: \($0)")
}
weather.temperature = 25


// Prints:
// 20.0 will set
// Temperature now: 20.0
// 20.0 will set
// Temperature now: 25.0

 

사용방법은 매우 간단합니다! 

프로퍼티 선언문 앞에 @Published 를 붙여주기만 하면 해당 프로퍼티 타입의 Publisher가 만들어지고,

해당 프로퍼티의 값이 바뀔 때마다 willSet 블록에서 publishing이 일어나게 됩니다.

willSet 블록에서 값이 publish 되기 때문에, 변경된 값이 프로퍼티에 실제로 설정되기 전에 구독자들은 새 값을 받을 수 있습니다.

사용하는 쪽에서는 $ 기호를 이용하여 projectedValue에 접근할 수 있습니다.

 

위의 예제 코드에서는 Double 타입의 temperature 프로퍼티 앞에 @Published를 붙여 관찰가능하도록 만들어주었습니다.

temperature를 .sink로 구독하게 되면, 값이 변경될 때마다 이벤트를 받을 수 있게 됩니다.

공식 문서의 예제 코드에서 프린트문을 하나 더 추가 해봤는데, willSet 시점에 publishing이 일어난다는 것이 더 눈에 잘 보이죠?? 😊

ObservableObject 프로토콜

프로퍼티를 관찰가능하게 만들어봤으니 이제 ObservableObject를 이용해 데이터 모델 자체를 관찰가능하도록 만들어볼게요!

  • 객체가 변경되기 전에 방출하는 pulisher(=objectWillChange)가 있는 객체 유형
  • 필수 구현을 필요로 하지 않는 프로토콜
  • 클래스에서만 사용 가능
  • @Published로 선언된 객체의 값이 변경될 때마다 objectWillChange.send()를 호출

SwiftUI로 개발을 하면서 굉장히 많이 사용하게 되는 친구가 이 ObservableObject 프로토콜입니다. 

(저의 경우, 뷰모델을 ObservableObject로 선언해 뷰에서 관찰가능하도록 만드는 데 많이 사용했습니다!)

모델 데이터를 관찰 가능하게 만들려면 데이터 모델이 ObservableObject를 준수하도록 하면 됩니다.

이를 뷰에서 관찰하면 데이터에 변경이 생길 때 마다 뷰를 업데이트 시켜줄 수 있습니다.

즉, 데이터가 변화하는 시점을 일일이 계산할 필요 없어지는 것이죠. (바인딩만 해주면 쉽게 뷰 업데이트 가능)

구체적인 동작 원리

어떻게 모델 자체를 Publishing 하고 구독하는 것이 가능할까요?

결론부터 말하자면, objectWillChange라는 Publisher를 통해 구독자들에게 변경을 알립니다.

ObservableObject 프로토콜을 따르게 되면 objectWillChange라는 프로퍼티를 가지게 되는데, 이는 Publisher입니다! 

objectWillChange는 ObservableObject의 인스턴스 내부에 선언된 @Published 타입의 프로퍼티의 값이 변경될 때마다 이벤트를 발생시킵니다. 더 자세히는, @Published로 선언된 프로퍼티의 willSet 블록에서 objectWillChange.send()를 호출하는 방식으로 publishing이 일어나게 됩니다.

 

이러한 원리로 ObservableObject에서는 데이터의 변경이 일어날 때마다 objectWillChange publisher를 발행하게 됩니다.

@Published 프로퍼티의 willSet에 연결되어 변경이 일어나기 때문에, ObservableObject를 구독하는 뷰는 프로퍼티의 값이 실제로 변경되기 전에 알림을 받아 뷰를 새로 고칠 수 있게 됩니다!

 

정리하자면,

1. @Published 프로퍼티의 값이 변경됨

2. willSet에서 objectWillChange.send() 호출

3. objectWillChange Publisher에서 publishing 일어남

4. 해당 ObervableObject 타입의 클래스를 구독하는 뷰에서 이벤트를 받아 뷰 업데이트

요런 방식으로 동작하는 것입니다~!

예제 코드

class Contact: ObservableObject {
    @Published var name: String
    @Published var age: Int


    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }


    func haveBirthday() -> Int {
        age += 1
        return age
    }
}


let john = Contact(name: "John Appleseed", age: 24)
cancellable = john.objectWillChange
    .sink { _ in
        print("\(john.age) will change")
}
print(john.haveBirthday())
// Prints "24 will change"
// Prints "25"

 

haveBirthday()에서 @Published로 선언된 age 프로퍼티의 값을 변경해주니, 이 ObservableObject 프로토콜 내부의 objectWillChange에서 퍼블리싱이 일어나겠죠??? .sink 블록 내부에서 확인할 수 있습니다!

 

SwiftUI로 개발하게 되면, @ObservedObject를 이용해 위와 같은 ObservableObject를 구독하고 데이터의 변경에 따라 뷰를 업데이트 시킬 수 있는데요, 이에 관한 내용은 다음 SwiftUI 게시글에서 다루도록 하겠습니다! 

 


[참고 자료]

 

Published | Apple Developer Documentation

A type that publishes a property marked with an attribute.

developer.apple.com

 

ObservableObject | Apple Developer Documentation

A type of object with a publisher that emits before the object has changed.

developer.apple.com