MVVM 아키텍처를 사용해서 앱을 구현했을 때 view model을 구독할 수 있는 property wrapper인 @StateObject 와 @ObservedObject 사이에서 헷갈렸던 적이 많이 있었기 때문에 둘 사이의 공통점과 차이점에 대해 기록하고자 한다.
# 🥨 Property Wrapper
우선, @StateObject 와 @ObservedObject 는 구독하고 있는 객체의 변경에 반응해서 화면을 업데이트할 수 있게 해주는 SwiftUI의 프로퍼티 래퍼이다. 두 프로퍼티 래퍼 모두 ObservableObject 프로토콜을 채택한 객체를 필요로 한다. 예를 들어 MVVM 아키텍처로 앱을 구현한다고 했을 때 ViewModel 프로퍼티의 변화를 View에 반영해주고 싶다면 ViewModel을 정의하는 클래스는 ObservableObject 프로토콜을 채택하고 있어야 View에서 @StateObject 혹은 @ObservedObject로 뷰모델 인스턴스를 바라볼 수 있다.
# 🥨 @ObservedObject 에 대한 이해
두 property warpper의 차이를 알아보기 위해서는 먼저 @ObservedObject 에 대한 이해가 필요하다. 위에서 말했듯 @ObservedObject 는 ObservableObject 프로토콜을 채택한 객체를 관찰해 주기 위한해주기위한 것이며, 해당 객체에서 @Published 로 정의된 속성이 바뀔 때마다 뷰를 업데이트시켜준다.
counter 코드 예시를 보면 이해하기 수월할 것이다. "Increment Counter" 버튼을 누름에 따라 @Published 어트리뷰트를 따르는 객체의 속성 (count)가 바뀌고 그 변화를 감지해서 뷰를 업데이트시켜준다.
final class CounterViewModel: ObservableObject {
@Published var count = 0
func incrementCounter() {
count += 1
}
}
struct CounterView: View {
@ObservedObject var viewModel = CounterViewModel()
var body: some View {
VStack {
Text("Count is: \(viewModel.count)")
Button("Increment Counter") {
viewModel.incrementCounter()
}
}
}
}
# 🥨 @StateObject
SwiftUI는 @StateObject를 선언하는 컨테이너의 수명 동안 모델 객체의 인스턴스를 한 번만 생성하며, @ObservedObject 와 거의 동일하게 동작하지만 꼭 필요한 경우가 따로 있다. 아마 이렇게 텍스트로만 이해하려 하면 어려울 것이고 상위뷰와 하위뷰가 있을 경우를 예로 들어 설명하면 쉬울 것 같다.
위의 counter 뷰를 자식뷰로 가지는 상위뷰를 만들면 아래와 같다. 상위뷰에서는 버튼을 눌렀을 때 @State 변수 값이 변경되고 그에 따라 뷰가 다시 그려진다. 다만 상위 뷰가 다시 그려지는 타이밍에 자식뷰의 count 숫자마저 초기화되는 이상한 점을 발견할 수 있다.
struct RandomNumberView: View {
@State var randomNumber = 0
var body: some View {
let _ = print("🎀 RandomNumberView 다시 그림")
VStack {
Text("Random number is: \(randomNumber)")
Button("Randomize number") {
randomNumber = (0..<1000).randomElement()!
}
}.padding(.bottom)
CounterView()
}
}
final class CounterViewModel: ObservableObject {
@Published var count = 0
func incrementCounter() {
count += 1
}
}
struct CounterView: View {
@ObservedObject var viewModel = CounterViewModel()
//@StateObject var viewModel = CounterViewModel()
var body: some View {
let _ = print("💜 CounterView 다시 그림")
VStack {
Text("Count is: \(viewModel.count)")
Button("Increment Counter") {
viewModel.incrementCounter()
}
}
}
}
이것은 하위뷰에서 @ObservedObject 를 통해 정의한 인스턴스는 화면이 새로 그려지게 되면 다시 초기화되기 때문이다.
@ObservedObject에 주석을 걸고 @StateObject의 주석을 풀어 다시 한번 테스트를 해보면 상위뷰에서 랜덤 값이 변경되어 화면이 새로 그려져도 count 값이 초기화되지 않는다. 이것은 하위뷰에서 @StateObject 를 통해 인스턴스를 생성했을 경우에 화면이 새로 그려져도 인스턴스는 그대로 유지되기 때문이다. 즉, @StateObject를 통해 관찰되고 있는 객체는 화면 구조가 재생성되어도 파괴되지 않고 안전하게 인스턴스를 유지할 수 있다는 것을 알 수 있다.
# 🥨 @StateObject 와 @ObservedObject, 언제 사용해 주면 될까
그럼 그냥 @StateObject 만 써주면 되는 게 아닌가 싶긴 하다. 나도 그러한 생각으로 @ObservedObject 보다는 @StateObject를 당연하게 써왔던 것 같다. 하지만 공식문서를 다시 읽으면서 느끼게 된 건 처음으로 ObservableObject 를 바라보게 되는 상위 뷰에서는 @StateObject를 사용하고 이후 하위 뷰에 내려주거나 할 때에는 @ObservedObject를 사용하는 것으로 활용하면 될 것 같다.
https://developer.apple.com/documentation/swiftui/observedobject
ObservedObject | Apple Developer Documentation
A property wrapper type that subscribes to an observable object and invalidates a view whenever the observable object changes.
developer.apple.com
참고
'SwiftUI' 카테고리의 다른 글
[SwiftUI] Local Notification 앱의 로컬알림 기능 구현 (0) | 2023.07.17 |
---|---|
[SwiftUI] 연속한 뷰에 대해 드래그 제스처로 on/off할 수 있는 기능 구현 | 드래그 제스처 .gesture DragGesture (0) | 2023.07.13 |
[SwiftUI] UIViewControllerRepresentable프로토콜로 갤러리에서 이미지 선택하기 (0) | 2023.06.20 |
[SwiftUI] Custom Picker로 textField 입력값 받기 (0) | 2023.06.12 |
SwiftUI에서 UIKit 사용하기 UIViewRepresentable(2) | @Binding 이란? Coordinator 란? (0) | 2023.06.09 |