-
[Swift] SwiftUI 프로퍼티 래퍼 뿌시기 3: @ObservedObject와 @StateObject 차이 이해하기테크 2024. 3. 7. 17:18
이전 시간에 Combine의 Publisher와 Subscriber에 대해서 초간단 설명을 드렸는데요.
이것만 딱 기억하시면 됩니다!
Publisher가 내보낸 값/이벤트만 Subscriber가 볼 수 있고
Subscriber만 Publisher가 내보낸 값/이벤트를 볼 수 있다저번 시간에 ObservableObject에서는 이벤트를 방출할 수 있다고 했죠?
그렇다면 이 이벤트를 받아볼 수 있는 구독자 즉 Subscriber가 있어야 하는데
내가 바로 Subscriber다! 라고 선언해주는 것이 @ObservedObject라고 했습니다.
@ObservedObject
@propertyWrapper @frozen struct ObservedObject<ObjectType> where ObjectType : ObservableObject
- ObservableObject를 따르는 클래스의 인스턴스를 생성할 때 사용하는 프로퍼티 래퍼
- @Observable로 감싸진 클래스를 @ObservedObject로 다시 감싸면 안됨 → 컴파일 에러 발생
- @Observable 객체를 바인딩 할 때는 @Bindable을 사용하는 것이 좋음
빨간색 글자는 지금은 몰라도 됩니다.
제가 다음에 @Observable 공부할 때 다시 말씀드리도록 할게요!
어쨌든 이미 @ObservedObject를 사용하는 방법을 대충 배웠는데요. [참고]
이번엔 좀 더 자세하게 알아보기 위해서 몇 가지 코드를 추가했습니다.
PlayerView에서는 random number를 생성하고 있습니다.
하위 View로는 CounterView가 있네요?
CounterView는 ObservableObject를 구독하고 있는 counter가 있습니다.
import SwiftUI class Counter: ObservableObject { @Published var count: Int = 0 func increaseCounter() { count = count + 1 } } struct CounterView: View { @ObservedObject private var counter = Counter() // Here! var body: some View { VStack { Button("Increase Counter") { counter.increaseCounter() } Text("\(counter.count)") } } } struct PlayerView: View { @State private var randomNumber = 0 var body: some View { VStack { Button("Generate Random Number") { randomNumber = (0..<1000).randomElement()! } Text("\(randomNumber)") CounterView() } } }
굉장히 단순한 코드같지만, 예상과는 다른 동작을 합니다...! (뭐가 문제야!!!)
코드에서 Increase Counter 버튼을 몇 번 클릭했다가
Generate Random Number 버튼을 클릭하게 되면
Counter의 count 값이 0으로 다시 초기화가 되는 기이한 일이 벌어집니다...!
PlayerView의 @State 변수가 바뀔 때 마다 하위 View들이 초기화되기 때문인데요.
이런 이유 때문에 Apple에서는 ObservedObject를 사용하는 것을 달가워하지 않습니다.
그렇다면 ObservedObject 말고 어떤 걸 사용하면 될까요?
정답은 @StateObject입니다!
@StateObject
@frozen @propertyWrapper struct StateObject<ObjectType> where ObjectType : ObservableObject
- ObservableObject를 따르는 클래스의 인스턴스를 생성할 때 사용하는 프로퍼티 래퍼
- 인스턴스는 single source of truth(SSOT)임 잘 몰라도 돼요!
- App, Scene, View에서 사용하는 것을 권고
- Private으로 사용하는 것을 권고
- View의 input이 바뀐다고 하더라도 새로운 인스턴스가 생성되지 않음
- 구조체, 문자열, 정수형 등을 저장할 땐 @State를 쓰는 것이 좋음
- @Observable 프로퍼티 래퍼로 클래스를 생성할 때도 @State를 쓰는 것이 좋음 [참고]
앞서 Single Source of Truth(SSOT)라는 용어를 썼는데요.
쉽게 말하자면 ObservableObject가 갖고 있는 값들은
다른 곳에 중복적으로 저장되어 있는 것이 아니라 해당 ObservableObject에만 있다고 생각하심 됩니다.
@StateObject를 사용할 때 새로운 인스턴스를 생성하는데요.
이 인스턴스는 View Identity가 바뀌지 않는 한 새로 생성되지 않습니다.
이 부분 때문에 @ObservedObject와 차이가 발생하는건데요.
바로 예제 코드 가시죠!
위의 코드에서 @ObservedObject를 @StateObject로만 바꿨습니다.
struct CounterView: View { @StateObject private var counter = Counter() // Here! var body: some View { VStack { Button("Increase Counter") { counter.increaseCounter() } Text("\(counter.count)") } } }
@ObservedObject를 @StateObject로 바꿔주게 되면
PlayerView에서 randomNumber 값이 바뀌어 하위 View가 initialization이 된다고 하더라도
@StateObject로 선언한 ObservableObject 인스턴스는 초기화가 되지 않습니다.
그럼 아까 전에 View의 Identity가 바뀌면 @StateObject가 초기화될 수 있다는 것을 간접적으로 말씀드렸는데요.
아래처럼 하위 View인 CounterView의 id를 random 값으로 설정하게 된다면
@State로 선언한 randomNumber 값이 바뀔 때마다 View의 id가 바뀌기 때문에
@ObservedObject처럼 매번 초기화시킬 수 있습니다.
struct PlayerView: View { @State private var randomNumber = 0 var body: some View { VStack { Button("Generate Random Number") { randomNumber = (0..<1000).randomElement()! } Text("\(randomNumber)") CounterView().id(randomNumber) // Here! } } }
@ObservedObject는 쓸모없나?
이렇게만 보면 @ObservedObject는 쓸모없는 것 처럼 느껴집니다...
하지만 @ObservedObject가 쓸 곳이 있기 때문에 여전히 deprecated 되지 않았겠져?
@ObservedObject는 아래처럼 @StateObject 값을 다른 View에서 사용할 때 사용할 수 있습니다!
MySubView에서 model의 isEnabled 값(=published property)이 바뀌게 되면
해당 property를 구독하고 있는 모든 Subscriber들은 이 변경사항을 실시간으로 전달받을 수 있습니다!
class DataModel: ObservableObject { @Published var name = "Some Name" @Published var isEnabled = false } struct MyView: View { @StateObject private var model = DataModel() var body: some View { Text(model.name) MySubView(model: model) } } struct MySubView: View { @ObservedObject var model: DataModel // Here! var body: some View { Toggle("Enabled", isOn: $model.isEnabled) // Here! } }
근데 Apple 문서를 보면 @ObservedObect를 대체할 수 있는 것이 @EnvironmentObject라고 하는데요.
이제 이걸 또 살펴봐야 하겠네요...
빨리 최근에 나온 @Observable 마스터하고 싶다ㅠ...!
Property Wrapper에 대해서 함께 공부하고 싶다면
#PropertyWrapper 태그로 들어가기!'테크' 카테고리의 다른 글