
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