[SwiftUI] StateObject, ObservedObject

강대훈·2025년 5월 18일
post-thumbnail

이번에 SwiftUI를 공부하면서 데이터를 전달할 때마다 거의 필수적으로 프로퍼티 래퍼를 사용해야 한다는 것을 알게 되었습니다.

SwiftUI에서도 MVVM 패턴을 사용하는데, ViewModel에 ObservableObject 를 채택해서 사용하는 경우가 매우 많습니다. 그리고 StateObject, ObservedObject는 ObservableObject 를 채택하고 있는 참조 타입을 가져야 합니다.

두 프로퍼티 래퍼 모두 다 데이터를 관리 및 변경할 수 있도록 하고 뷰를 업데이트를 자동으로 해준다는 것은 알고 있지만, 그렇다면 두 프로퍼티 래퍼의 차이는 무엇이고 어떻게 사용해야 할까요?

한 번 공식문서를 통해서 번역하고 정리해봤습니다.

StateObject

View에서 참조 타입을 소유하고 관리하기 위한 PropertyWrapper

값 타입에서는 State-Binding 조합을 통해서 뷰에서 데이터를 소유하고 관리 및 추적할 수 있었습니다. MVVM 패턴에서는 보통 뷰모델을 Class로 사용하기 때문에 State를 사용하기에는 어려움이 있습니다. 그래서 나온 것이 참조 타입을 소유하고 관리할 수 있는 StateObject입니다.

왜 ViewModel에서는 참조 타입을 사용할까?

만약 여러 뷰에서 하나의 뷰모델을 사용하게 된다면 인스턴스를 공유함을 통해서 데이터를 공유해야 하기 때문입니다. 만약 값 타입으로 뷰모델을 사용한다면, 뷰모델을 소유하는 뷰가 많아질수록 복사가 자주 일어나기 때문에 오히려 비효율적일 수 있습니다.

또한 SwiftUI의 중요한 개념인 하나의 값은 하나의 공급원을 가져야 한다. 라는 개념은 아무래도 하나의 공급원이 인스턴스를 공유해서 사용할 수 있는 참조 타입을 뜻하는 것으로도 보입니다.

class DataModel: ObservableObject {
    @Published var name = "Kang"
    @Published var isEnabled = false
}

struct MyView: View {
    @StateObject private var model = DataModel()

    var body: some View {
        Text(model.name)
        Text(model.isEnabled ? "True" : "False")
    }
}

다음과 같이 소유하고자 하는 참조 타입 데이터에 ObservableObject 를 채택하고, 추적 및 관리하고자 하는 인스턴스 프로퍼티에 @Published 를 선언해야 합니다. 물론 @Published 를 사용하지 않더라도 데이터는 변경할 수 있지만, SwiftUI가 뷰를 업데이트하지는 못합니다.

StateObjectObservableObject 를 채택하고 있는 참조 타입에만 적용이 가능합니다. 이런 간단한 방식만으로 DataModel 에 있는 데이터를 추적 및 관리하고 소유하며, 해당 데이터가 변경되었을 때, 뷰를 업데이트할 수 있습니다.

만약 하위뷰에서 뷰모델의 인스턴스 프로퍼티를 변경하게 된다면, 해당 인스턴스 프로퍼티를 사용하는 모든 뷰에서 업데이트가 발생합니다. 그러면 하위뷰에 뷰모델을 전달하면 어떻게 될까요?

struct MyInitializableView: View {
    @StateObject private var model = DataModel()

    var body: some View {
        MySubView(name: model.name)
    }
}

struct MySubView: View {
    @StateObject private var model: DataModel

    init(name: String) {
        _model = StateObject(wrappedValue: DataModel(name: name))
    }

    var body: some View {
        Text("Name: \(model.name)")
    }
}

이런 경우에는 뷰모델의 인스턴스 프로퍼티인 name의 데이터를 상위뷰에서 변경하더라도 하위뷰 MubView의 뷰가 업데이트되지 않습니다. 가장 큰 문제는 서로 같은 인스턴스가 아니라, 하위뷰에서는 새로운 초기화된 인스턴스를 사용한다는 것입니다. 또한 StateObject로 선언된 인스턴스를 넘겨주는 것도 불가능합니다.

그래도 만약에 강제로 데이터가 바뀔 때 다시 초기화하기를 원한다면 다음과 같은 방법이 있습니다.

var body: some View {
    MubView(name: model.name)
		    .id(model.name)
}

강제로 뷰의 identity 를 변경하여 다시 초기화하는 방식으로 사용할 수 있습니다.

StateObject는 성능상 이점을 보기 위해서 의도적으로 한 번만 초기화를 진행합니다. 그렇기 때문에 외부 데이터로 초기화할 때는 View가 업데이트 되지 않을 수 있다는 점을 기억할 필요가 있습니다.

그렇다면 하위뷰가 뷰모델을 전달받을 수 있는 방법은 어떤 방법이 있을까요?

ObservedObject

ObservableObject를 관찰하고, 데이터가 변경될 때 View를 업데이트하는 Property Wrapper

보통 상위 뷰의 StateObject를 하위 뷰에서 전달받고 싶을 때 사용합니다.

상위 뷰와 하위 뷰 모두가 StateObject를 사용할 때의 문제점이 있었습니다. 초기화 하는 과정에서 동일한 인스턴스를 사용하지 않기 때문에 같은 데이터를 공유하지 않는다는 점입니다.

(정확한 말로는 뷰모델 인스턴스를 전달받을 수가 없습니다.)

그렇기 때문에 EnvironmentObject 를 통해서 뷰모델을 공유하고는 했습니다. 하지만 이 방법도 전역으로 공유되는 방식이기 때문에 때에 따라서는 역할과 범위가 모호해질 수 있다는 단점이 있고, 크래쉬의 위험이 있습니다.

상위 뷰의 뷰모델을 직접 주입받을 수 있고, 외부에서 주입받은 뷰모델의 상태를 함께 추적할 수 있는 방법이 바로 ObservedObject 를 사용하는 것입니다. 어떻게 사용하는지 예시를 봅시다.

struct MyInitializableView: View {
    @StateObject private var model = DataModel()

    var body: some View {
        Text("Name: \(model.name)")
        MySubView(model: model)
        Button {
            model.name = String((0...10).randomElement()!)
        } label: {
            Text("Hi")
        }
    }
}

struct MySubView: View {
    @ObservedObject private var model: DataModel

    init(model: DataModel) {
        self.model = model
    }

    var body: some View {
        Text("Name: \(model.name)")
    }
}

다음과 같이 하위 뷰에 직접 뷰모델을 주입해서 사용할 수 있습니다. ObservedObject를 통해서 상위 뷰의 뷰모델을 추적하고 있기 때문에, 하위 뷰에서 뷰모델의 데이터를 변경하면 상위 뷰와 하위 뷰 모두 View가 업데이트 될 수 있습니다.

쓰임새의 차이?

마지막으로 이번에 공부하면서 두 프로퍼티 래퍼의 가장 큰 차이는 직접 뷰모델을 소유하고 관리하는 뷰에서 StateObject를 사용하고, 외부로부터 주입받아서 사용하고 뷰모델을 관리할 책임이 없다면 ObservedObject를 사용합니다.

많은 사람들이 차이점을 얘기할 때 간단히 상위뷰에서는 StateObject, 하위뷰에서는 ObservedObject를 사용한다고 하지만, 많이 사용하는 만큼 명확한 차이에 대해서는 알고 있어야 할 거 같습니다.

0개의 댓글