본문 바로가기

iOS/SwiftUI

[SwiftUI] Managing Data by Property Wrappers (@State, @StateObject, @ObservedObject, @EnvironmentObject)

SwiftUI가 기존 UIKit과 비교하여 크게 다른 점 중 하나는, State를 관리할 수 있는 Property Wrappers가 기본적으로 제공된다는 것입니다. 가장 기본적인 @State부터 시작하여, @StateObject, @ObservedObject, @EnvrionmentObject까지 언뜻 비슷하지만 확연히 다른 성질을 가지고 있습니다.
 
특성과 용도에 대해 헷갈릴 수 있는 부분이 있어 위의 4가지 프로퍼티 래퍼에 대해 간단히 요약해보려고 합니다.
 
먼저 @State에 대해 알아보겠습니다.

@State

A property wrapper type that can read and write a value managed by SwiftUI.
@frozen @propertyWrapper struct State<Value>

@State는 SwiftUI에 의해 관리되는 값을 읽고 쓸 수 있는 프로퍼티 래퍼 타입이라고 합니다.
 
좀 더 직관적으로, State는 말 그대로 View의 상태값입니다. @State로 선언된 변수의 값이 바뀌면 view는 자신의 계층 구조에 포함되는 하위 뷰를 포함하여 view를 다시 그립니다. 그렇기에 @State는 그 자체로 최신값을 보장할 수 있습니다. 애플은 @State가 뷰 내부에서만 사용되는 상태이기 때문에, private으로 선언하며 memberwise initializer로 초기화되는 것을 막아줄 것을 권장합니다.
 
앞으로 나올 프로퍼티 래퍼와 크게 구분되는 점 중 하나는, @State가 간단한 값 타입을 저장하기 위한 래퍼라는 것입니다. String, Int, Bool과 같은 값들을 저장하며, 참조 타입을 사용하기 위해서는 뒤에 나올 래퍼들을 사용하면 됩니다.
 
 

출처: apple developer

@State는 value를 읽을 수단으로 프로퍼티 래퍼이기 때문에 wrappedValue와 projectedValue를 제공합니다. wrappedValue란 말 그대로 래퍼가 가지고 있는 값 그 자체이며, 기존에 변수에 접근하는 것과 같은 방식으로 변수 이름을 통해 접근할 수 있습니다. 만약 하위 뷰에서 값에 바인딩이 필요하여 상태 자체가 필요한 경우가 있다면, $를 붙여서 projected value인 Binding<>를 전달하여 상태를 공유할 수 있습니다.
 
마지막으로, 공식 문서에 따르면 App, Scene, View에서 State를 초기화하여 single source of truth로 사용할 것을 권장합니다. single source of truth는 애플에서 만든 말이 아니고, 정보 시스템 설계 이론 중 하나로 스키마를 오직 하나의 출처에서만 생성하고 편집하도록 하는 방법론이라고 합니다. 간단하게 SSOT가 지켜지지 않는 상황을 생각해 보면, 데이터가 곳곳에 분산되어 있을 때 어느 곳에 있는 데이터를 신뢰해야 하는지 결정하기 어려워지며, 개발 과정에서도 유지보수가 힘들어 질 것을 예상할 수 있습니다.

@ObservedObject

A property wrapper type that subscribes to an observable object and invalidates a view whenever the observable object changes.
@propertyWrapper @frozen struct ObservedObject<ObjectType> where ObjectType : ObservableObject

 
@ObservedObject는 observable object를 구독하고 observable object가 변화할 때마다 뷰를 invalidate하는 프로퍼티 래퍼 타입이라고 합니다. @ObservedObject는 View가 re-render 될 때에 함께 초기화됩니다. 이는 추후 설명할 @StateObject와 주요한 차이점이 됩니다.
 
우선 Observable object란, 참조 타입 내부에 @Published로 선언된 변수들의 변화를 관찰할 수 있는 publisher를 제공하는 프로토콜입니다. @ObservedObject와 뒤에 나올 @StateObject는 ObservableObject를 이용하여 복잡한 참조 타입에 대한 상태를 관찰할 수 있게 해줍니다. 사용 방식은 @State와 마찬가지로, wrapepd value와 projected value를 이용하면 됩니다.
 
공식 문서에서는 View의 parameter에 Observable Object를 input으로 넣어준 다음, ObservableObject 내부의 @Published 프로퍼티의 변화에 반응하여 뷰를 업데이트 할 수 있다고 합니다. 전형적으로 @StateObject를 선언한 다음 이를 하위 View의 initializer로 전달하는 방식이 있을 거라고 예상하고 있습니다.
 
Don’t specify a default or initial value for the observed object. Use the attribute only for a property that acts as an input for a view, as in the above example.
 
또한 위와 같이, observed object에 초기값을 할당하고 생성하지 않도록 권장합니다. 왜 특정 view에 대한 input으로만 @ObservedObject를 사용해야 하는지 알기 위해서는 @StateObject를 함께 봐야 할 것 같습니다.

@StateObject

A property wrapper type that instantiates an observable object.
@propertyWrapper @frozen struct ObservedObject<ObjectType> where ObjectType : ObservableObject

@StateObject는 observable obejct를 객체화하는 프로퍼티 래퍼 타입이라고 합니다. 프로퍼티 래퍼의 선언은 @ObservedObject와 동일한 모습인 것을 확인할 수 있습니다.
 
애플이 내린 정의에서 눈에 띄는 차이점은 observable object를 객체화한다는 것입니다. 객체화란 정확히 어떤 일을 칭하는 것일까요?
 
공식 문서에서 @ObservedObject와 눈에 띄는 차이점은, @StateObject에서는 @State와 동일하게 single source of truth로 사용하라는 말이 있다는 것입니다. 단지 그 대상이 값 타입에서 레퍼런스 타입으로 바뀌었을 뿐입니다. 또한 @State와 같이 private으로 선언하여 외부의 접근으로 인한 conflict를 막으라고 합니다.
 
SwiftUI는 State Object로 선언된 객체를 container의 lifetime 동안 오직 한 번만 인스턴스화 한다고 합니다. 예를 들어 view의 identity가 바뀔 경우에만 새로운 인스턴스를 생성하며, view의 input이 변화하는 경우에는 새 인스턴스를 만들지 않습니다. 예시를 통해 자세히 보겠습니다.

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

    init(name: String) {
        // SwiftUI ensures that the following initialization uses the
        // closure only once during the lifetime of the view, so
        // later changes to the view's name input have no effect.
        _model = StateObject(wrappedValue: { DataModel(name: name) }())
    }

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

위와 같은 custom view를 작성할 때, @StateObject에 초기값을 전달하고 싶을 수 있습니다. 이러한 경우를 위해 StateObject 생성자를 제공하며, Model에 파라미터로 값을 전달하는 식으로 인스턴스를 생성할 수 있습니다.
 
주의할 점은, MyInitializableView의 상위 뷰에서 name에 대한 input이 변경되며 뷰를 다시 그려야 할 때, @StateObject는 새로운 인스턴스를 생성하지 않습니다. @StateObject의 상태는 격리되어 보관되며, 새로 뷰를 그릴 때 다시 참조하는 것입니다. 이것이 @StateObject가 input에 대해 새로운 인스턴스를 생성하지 않는다는 것입니다.
 
따라서 명시적으로 State Object를 생성하는 방식은, 불변값을 전달해야 할 때 유용할 것입니다.
 
그러나 앞서 view으 identity가 변하는 경우 State Object는 새 인스턴스를 만든다고 했습니다. 

MyInitializableView(name: name)
    .id(name) // Binds the identity of the view to the name property.

위와 같은 방식으로 id modifier를 이용하면 State Object도 명시적으로 새 인스턴스를 만들게 할 수 있습니다.

@StateObject와 @ObservedObject의 사용처 비교

지금까지 공식 문서의 내용을 살펴본 바에 의하면, @StateObject는 View의 identity에 따라 오직 한 번만 인스턴스화 되지만 @ObservedObject는 View가 다시 그려질 때마다 재생성 된다는 것을 알 수 있습니다. 그리고 @StateObject를 subviews의 parameter를 통해 ObservedObject로 전달하는 방식을 권장한다는 것 또한 확인했습니다.
 
이러한 차이는 두 상태의 life cycle과 관련이 있습니다. SwiftUI에서 상위 View의 상태가 변화하면 뷰가 아예 다시 그려집니다. 하위 뷰는 그럴 때마다 재생성 될 것이기에, 생성 비용이 클 수 있는 외부 객체를 매번 재생성하게 되면 성능 측면에서 좋지 않을 것입니다. 따라서 메모리 관리 측면을 생각하여 State는 한 번만 인스턴스화 되어 유지하고, 이를 공유해야 하는 하위 뷰에서는 파라미터를 통해 참조만 전달받아 사용하는 것이 유리할 것입니다.

@EnvironmentObject

A property wrapper type for an observable object supplied by a parent or ancestor view.
@frozen @propertyWrapper struct EnvironmentObject<ObjectType> where ObjectType : ObservableObject

@EnvironmentObject는 parernt 또는 ancestore view에서 제공된 observable object에 대한 프로퍼티 래퍼 타입이라고 합니다. @StateObject, @ObservedObject와 마찬가지로 Observable Object이기 때문에 참조 타입의 상태 변화에 따라 View가 다시 그려집니다.
 

WWDC 2020

 
@EnvrionmentObject의 핵심은, 부모 뷰 또는 조상뷰에서 제공하는 observable object를 이용할 수 있도록 하는 간단한 인터페이스를 제공한다는 것입니다. .environmentObject로 Observable Object를 특정 View의 환경에 전달할 수 있으며, 이는 모든 하위 뷰들에서 이 object에 접근을 가능하게 합니다. 위의 그림과 같이 계층 사이의 view에서는 이 값의 존재에 대해 알 필요가 없기 때문에 조금 더 쾌적하고 편리한 코드를 작성할 수 있습니다.
 
주의할 점은 환경 값에 대한 접근이 런타임에 이루어지기 때문에, 컴파일 타임에 체크할 수 없어 환경에 값이 존재하지 않으면 크래시가 날 수 있다는 것입니다.

맺으며

WWDC 2020

위 사진은 WWDC 2020: Data Essentials in SwiftUI의 발표 자료입니다.
 
@ObservedObject는 dependency를 만들기 위해 사용하며,
@StateObject는 ObservableObject를 View의 life cycle에 연결하기 위해 사용하고,
@EnvironmentObject는 ObservableObject에 인체공학적(편리하게)으로 접근할 수 있는 도구로 사용합니다.
 
@State를 포함한 프로퍼티 래퍼들의 특징을 이해하고 적절히 사용하는 것이 프로덕트의 성능 및 유지보수성에 큰 도움이 될 것 같습니다.

참조

https://www.avanderlee.com/swiftui/stateobject-observedobject-differences/

 

@StateObject vs. @ObservedObject: The differences explained

@StateObject and @ObservedObject have similar characteristics but differ in an important way which can lead to unexpected bugs.

www.avanderlee.com

https://levelup.gitconnected.com/state-vs-stateobject-vs-observedobject-vs-environmentobject-in-swiftui-81e2913d63f9

 

SwiftUI: @State vs @StateObject vs @ObservedObject vs @EnvironmentObject

One of the first decisions SwiftUI developers need to make is which of the available property wrappers to use to store data. Especially in…

levelup.gitconnected.com