SwiftUI

[SwiftUI] @StateObject 와 @ObservedObject

하이D:) 2023. 6. 21. 14:49

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 에 대한 이해가 필요하다. 위에서 말했듯 @ObservedObjectObservableObject 프로토콜을 채택한 객체를 관찰해 주기 위한해주기위한 것이며, 해당 객체에서 @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] @StateObject, @ObservedObject

@StateObject vs. @ObservedObject: The differences explained