SwiftUI를 이용해서 MVVM 패턴으로 개발을 하다보면 View에서 관찰 가능한 ViewModel 인스턴스를 만들때 @StateObject
, @ObservedObject
property wrapper를 사용하게 된다.
프로젝트를 진행 하면서도 항상 둘 중에 무엇을 사용해야 되는가 고민을 하게 되는데 이번 기회에 @StateObject
와 @ObservedObject
의 공통점과 차이점, 그리고 어떤 상황에서 무엇을 사용해야 되는지 깔끔하게 정리해 보자
시작에 앞서 @ObservedObject
property wrapper가 무엇인지 알아보자
View에서 @ObservedObject
로 선언된 인스턴스는 해당 인스턴스가 가지고 있는 @Published
변수가 값이 변경 되었을때 이 변화를 감지하고 View를 새롭게 재렌더링할 수 있게 한다.
@ObservedObject
로 선언할 수 있는 객체는 ObservableObject
protocol을 따르는 클래스 타입의 인스턴스만을 선언할 수 있다.
아래 예시를 통해 다시 한번 알아보자
final class CounterViewModel: ObservableObject {
@Published var count: Int = 0
func addCount() {
count += 1
}
}
CounterViewModel
class는 ObservableObject
protocol를 채택하고 있으며, count
변수는 @Published
property wrapper로 선언되어 있어 count
변수의 값이 변경된다면 SwiftUI에게 데이터가 변경 되었음을 알리고 CounterViewModel
class의 인스턴스와 연관 되어 있는 View를 새롭게 렌더링할 수 있다.
정확히 말하자면 값이 변경되면 ObservableObject
protocol에서 제공하는 objectWillChange
publisher의 send()
라는 메서드를 자동으로 호출하여 데이터가 변경되었음을 알릴 수 있다.
import SwiftUI
struct CounterView: View {
@ObservedObject var viewModel: ViewModel = ViewModel()
var body: some View {
VStack(spacing: 20) {
Text("viewModel.count : \(counterViewModel.count)")
Button(action: {
counterViewModel.addCount()
}, label: {
Text("Add viewModel count")
})
}
}
}
View에 해당하는 CounterView
에서는 counterViewModel
변수를 @ObservedObject
property wrapper로 선언 하였으며, Button
을 터치하게 되면 counterViewModel
의 addCount
메서드를 호출하여 counterViewModel
의 count
변수를 1씩 증가 시키고 연관되어 있는 View를 새롭게 렌더링하게 될것이다.
그렇다면 @StateObject
property wrapper는 무슨 역할을 할까??
목적 자체는 @ObservedObject
와 마찬가지로 해당 인스턴스가 가지고 있는 @Published
변수가 값이 변경되었을 때 이 변화를 감지하고 View를 새롭게 렌더링하는 것으로 동일하다.
따라서 위의 코드에서 @ObservedObject
를 @StateObject
로 변경해도 결과는 동일하게 동작한다.
import SwiftUI
struct CounterView: View {
@StateObject var counterViewModel: CounterViewModel = CounterViewModel()
var body: some View {
VStack(spacing: 20) {
Text("viewModel.count : \(counterViewModel.count)")
Button(action: {
counterViewModel.addCount()
}, label: {
Text("Add viewModel count")
})
}
}
}
이제부터 혼란이 시작된다..
그럼 @ObservedObject와 @StateObject 중에 무엇을 사용해야 되지..?? 그냥 @ObservedObject 쓰면 되는거 아닌가?
두 property wrapper에는 명확한 차이가 존재하며, 이 차이를 체감하기 위해서 CounterView
를 상위 View가 포함하고 있는 형태에서 차이를 확인할 수 있었다.
가령 다음과 같이 CounterView
를 포함하고 있는 ContentView
에서 두 property wrapper의 차이를 확인 해보자
import SwiftUI
struct ContentView: View {
@State private var count: Int = 0
var body: some View {
VStack(spacing: 20) {
Text("count: \(count)")
Button(action: {
count += 1
}, label: {
Text("Add")
})
CounterView()
}
}
}
ContentView
는 @State
property인 count
변수를 가지고 있으며 Button
을 터치하면 count
의 값이 1씩 증가하고 데이터가 변경됨에 따라 View를 새롭게 렌더링 한다.
다음으로 CounterView
의 경우 CounterViewModel
클래스 인스턴스를 @ObservedObject
property wrapper로 가지고 있다.
import SwiftUI
struct CounterView: View {
@ObservedObject var counterViewModel: CounterViewModel = CounterViewModel()
//나머지는 위의 코드와 동일
}
이때 ContentView
의 결과를 확인해보면 다음과 같다.
결과를 확인해보면서 좀 당황스러운 부분은 CotentView
에 속한 @State
property인 count
의 값을 변경하였을때 CounterView
에 속한 counterViewModel
의 counter
가 초기화 되버린다는 것이다.
이 현상의 윈인은 다음과 같다.
CotentView
에 속한count
의 값을 변경하였을때 View를 다시 랜더링하면CounterView
도 새로 그려지면서 값이 초기화 되는것..
이번에는 CounterView
에 속한 counterViewModel
을 @StateObject
로 변경하고 결과를 다시 확인해보자
import SwiftUI
struct CounterView: View {
@StateObject var counterViewModel: CounterViewModel = CounterViewModel()
//나머지 코드는 위와 동일
}
이번에는 CotentView
에 속한 count
의 값을 변경되어도 CounterView
에 속한 CounterViewModel
의 counter
property의 값이 초기화되지 않는다는 점이다.
위의 예시를 통해 우리는
@StateObject
로 선언 되어 있으면 View가 재렌더링 되더라도 초기화 되지 않는구나
를 확인해 볼 수 있었다.
여기서 확인해 봐야할 부분은 과연 @StateObject
를 포함하는 하위 View는 재렌더링 되지 않는것을까? 아니면 재렌더링 되더라도 값이 유지 되는 것일까?
결론부터 말하면 재렌더링 되더라도 값이 유지 되는 쪽에 더 가깝다고 할 수 있으며 이것을 확인하기 위해 CounterView
내부에 initializer를 만들어 View가 재렌더링 되는지 확인해 보았다.
import SwiftUI
struct CounterView: View {
@StateObject var counterViewModel: CounterViewModel = CounterViewModel()
init() {
print("CounterView initialized")
}
var body: some View {
VStack(spacing: 20) {
Text("viewModel.count : \(counterViewModel.count)")
Button(action: {
counterViewModel.addCount()
}, label: {
Text("Add viewModel count")
})
}
}
}
그리고 결과 확인을 위하여 위와 같이 ContentView
에서 버튼을 터치하여 결과를 확인 해보았을때 아래와 같이 하위 View인 CounterView
가 다시 생성되는 것을 확인할 수 있었다.
결국 @StateObject
property 변수를 가지고 있더라도 상위 View에서 값이 변경되면 마찬가지로 초기화 되지만 그 값은 유지 된다는 점이 @ObeservedObject
와 @StateObject
의 가장 큰 차이점이라고 할 수 있겠다.
지금까지 @ObeservedObject
와 @StateObject
두 property wrapper의 차이점이 무엇인지 살펴 보았다.
마지막으로 어떤 상황에서 어떤 property wrapper를 사용하면 좋을지에 대해 이야기 해보고 마무리하려고 한다.
필자는 두 property wrapper의 공통점과 차이점을 토대로 다음과 같이 정리하고 싶다.
상위 View의 값의 변화로 하위 View의 내용이 모두 새롭게 초기화 되어야 한다 -> @ObservedObject
사용
상위 View의 값의 변화에 따라 하위 View에는 영향이 없이 그래도 유지되거나 새로운 값만 View에 추가적으로 렌더링 되어야 한다. -> @StateObject
사용
https://pilgwon.github.io/post/state-object-vs-observed-object