@StateObject와 @ObservedObject의 차이와 선택 기준
@StateObject vs. @ObservedObject: The differences explained
핵심 요약
- 두 래퍼 모두 ObservableObject의 변경을 관찰하지만, 뷰가 객체의 생명주기를 소유하고 보존하는 역할은 @StateObject에만 있다.
- 뷰가 객체를 직접 만들면 @StateObject를 사용하고, 부모 등 외부에서 전달받으면 @ObservedObject를 사용해야 한다.
- iOS 17 이상을 대상으로 하는 새 코드에서는 @Observable과 @State를 고려할 수 있으며, 주입된 모델에는 필요에 따라 @Bindable을 사용한다.
도입
SwiftUI에서 @StateObject와 @ObservedObject는 모두 ObservableObject의 변경을 관찰해 뷰를 갱신한다. 차이는 객체의 소유권과 생명주기 보존에 있다. 뷰가 객체를 직접 생성하면 @StateObject가 해당 뷰의 정체성에 연결된 저장 공간을 제공한다. 반대로 객체를 부모 뷰에서 전달받으면 @ObservedObject로 변경을 관찰한다. 잘못 선택하면 뷰 갱신 과정에서 객체가 다시 만들어져 상태가 초기화될 수 있다.
상세 요약
선택 기준은 객체의 소유권
@StateObject와 @ObservedObject는 모두 ObservableObject를 관찰하고, 객체의 @Published 프로퍼티가 바뀌면 관찰 중인 뷰를 다시 평가한다. 차이는 객체의 생명주기를 누가 관리하느냐에 있다.
- 뷰가 객체를 생성하고 소유하면
@StateObject private var를 사용한다. - 뷰가 부모 등 다른 곳에서 객체를 전달받으면
@ObservedObject var를 사용한다. - 여러 형제 뷰가 한 객체를 관찰하면 부모가 객체를 소유하고 각 자식에게 같은 인스턴스를 전달한다.
@ObservedObject: 전달받은 객체 관찰
@ObservedObject는 다른 뷰가 소유하는 ObservableObject를 관찰할 때 사용한다. 예를 들어 CounterView가 초기화를 통해 CounterViewModel을 전달받는다면, 이 뷰는 객체의 생명주기를 소유하지 않으므로 @ObservedObject가 적절하다. 모델의 @Published 값이 바뀌면 CounterView가 갱신된다.
@ObservedObject 프로퍼티에 객체를 인라인으로 생성하는 방식은 피해야 한다. 이 래퍼는 해당 객체의 생명주기를 보존할 저장 공간을 제공하지 않는다. 따라서 부모가 갱신되어 하위 뷰 값이 다시 만들어지면, 인라인으로 생성한 모델도 교체될 수 있다. 원문의 예시에서는 부모의 난수를 변경할 때 하위 카운터 모델이 다시 만들어져 카운트가 초기화된다.
@StateObject: 뷰가 소유하는 객체
뷰가 모델을 직접 생성한다면 @StateObject private var viewModel = CounterViewModel()처럼 선언한다. SwiftUI가 뷰 값을 여러 번 다시 만들어도, @StateObject는 뷰의 정체성에 연결된 모델을 보존한다. 따라서 동일한 CounterViewModel 인스턴스가 갱신 사이에도 유지된다.
private 선언은 소유권을 코드에 드러낸다. 부모가 다른 초기값을 주입해도 이후 SwiftUI가 그 값을 무시하는 상황을 막는 데도 도움이 된다. 반대로 모델 소유권을 부모에 두고 자식에게 전달하면 자식은 @ObservedObject를 사용한다.
ObservableObject와 최신 Observation
ObservableObject와 @StateObject는 대체 관계가 아니다. ObservableObject는 모델이 채택하는 프로토콜이고, @StateObject는 SwiftUI 뷰가 그 모델을 소유할 때 사용하는 프로퍼티 래퍼다. 모델은 ObservableObject를 따르고, 뷰는 소유권에 따라 @StateObject 또는 @ObservedObject를 선택한다.
iOS 17 이상 대상의 새 코드에서는 Observation 프레임워크의 @Observable과 @State 조합을 고려할 수 있다. 이 방식에서는 ObservableObject, @Published, @StateObject를 각각 대체한다. 주입된 @Observable 모델은 대체로 프로퍼티 래퍼 없이 사용할 수 있고, 자식 뷰에서 모델 프로퍼티의 바인딩이 필요할 때 @Bindable을 추가한다. 구형 운영체제를 지원하거나 기존 ObservableObject 모델을 사용하는 경우에는 기존 래퍼가 계속 관련된다.
뷰 모델의 책임과 도입 기준을 함께 정리하려면 SwiftUI에서 과도한 추상화 없이 MVVM 적용하기를 참고할 수 있다.
결론
@StateObject와 @ObservedObject 중 무엇을 쓸지는 객체를 만드는 주체로 결정한다. 소유 뷰에는 private @StateObject를 선언하고, 의존성을 전달받는 하위 뷰에는 @ObservedObject를 둔다. 여러 형제 뷰가 같은 객체를 관찰한다면 부모가 한 번 소유한 뒤 같은 인스턴스를 각각 전달한다. iOS 17 이상 대상의 새 코드에서는 @Observable과 @State 조합을 사용할 수 있으며, 하위 뷰가 바인딩을 요구할 때만 @Bindable을 추가한다.