Sure, Why not?

@StateObject, @ObservedObject, @Environment, @EnvironmentObject (프로퍼티 래퍼 - 3) 본문

💻

@StateObject, @ObservedObject, @Environment, @EnvironmentObject (프로퍼티 래퍼 - 3)

joho2022 2026. 1. 6. 21:00

 

 

 

@StateObject

View 계층에서 ObservableObject의 SSoT 역할을 한다.
 

StateObject | Apple Developer Documentation

A property wrapper type that instantiates an observable object.

developer.apple.com

 

View, Scene, App에서 사용 가능하며,

ObservableObject를 직접 생성한다. 

 

SwiftUI에서 @StateObject를 생성하는 경우는

View의 Identity가 바뀔 때 새 인스턴스가 생성된다.

 

View의 입력 값이 변경되거나 다시 렌더링되는 경우는

새 인스턴스가 생성되지 않는다.

즉, View가 살이있는 동안 @StateObject는 단 한번만 생성된다.

 

- Struct

- String

- Int

와 같은 값 타입을 저장할 때는 State를 사용하자.

(번외로 iOS17 이상 사용가능한 Observable() 매크로도 마찬가지)

 

상위 @StateObject를 하위 View와 상태 공유하기

1. ObservedObject 전달

struct ChildView: View {
    @ObservedObject var model: DataModel
}

 

 

2. Environment에 주입 (@EnvironmentObject)

struct MySubView: View {
    @EnvironmentObject var model: DataModel

    var body: some View {
        Toggle("Enabled", isOn: $model.isEnabled)
    }
}

 

 

Binding 얻기 - $

@StateObject의 프로퍼티는
$ 연산자를 통해 Binding으로 접근 가능하다.

Toggle("Enabled", isOn: $model.isEnabled)

 

 

외부데이터로 StateObject 초기화하기

StateObject 초기 값이 View 외부 데이터에 의존할 때

-> View의 init 내부에서 초기화를 명시하면 된다.

 

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

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

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

SwiftUI는 해당 초기화 클로저를 한 번만 실행하고, 이후 name 값이 바뀌어도 model를 다시 생성되지 않는다.

 

이때, 외부 데이터로 초기화하더라도, private 사용하는 것을 권장한다.

- View initalizer를 통한 직접 주입 방지

- framework’s storage management  충돌 방지

- 예기치 않은 상태 리셋 방지하기 위해서

 

 

View Identity 변경으로 재초기화 하기

입력 값이 바뀔 때마다 StateObject를 다시 만들고 싶다면?

-> id(_:) 를 사용하자

 

MyInitializableView(name: name)
    .id(name)

name이 바뀌면 재생성됨

 

 

var hash: Int {
    var hasher = Hasher()
    hasher.combine(name)
    hasher.combine(isEnabled)
    return hasher.finalize()
}

MyInitializableView(name: name, isEnabled: isEnabled)
    .id(hash)

여러 값으로 identity 만들 수 있다.

 

물론 Identity변경으로 인한 부작용이 있기 때문에, 정말 필요할 때만 사용하기를 권장하고 있다.

 

 


 

 

@ObservedObject

ObservableObject를 구독하고, 그 객체가 변경될 때마다 View를 다시 그리게 만드는 property wrapper
 

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

 

View가 ObservableObject를 직접 생성하지 않고, 외부에서 전달받아 사용하는 상황에 적합.

-> StateObject를 하위 View로 전달할 때

 

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


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


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


struct MySubView: View {
    @ObservedObject var model: DataModel


    var body: some View {
        Toggle("Enabled", isOn: $model.isEnabled)
    }
}

 

ObservableObject의 @Published 프로퍼티가 변경되면,

그 객체를 관찰 중인 모든 View를 갱신 함.

하위 View에서도 변경할 수 있고, 그 변경은 View 계층 전체에 전파된다.

 

ObservedObject 사용 시 주의사항

기본 값 또는 초기값을 지정하지 말 것.

// ❌ 잘못된 사용
@ObservedObject var model = DataModel()

반드시 외부에서 주입되어야 한다.

 

Observable과 함께 쓰면 안 되는 이유

Observable 매크로 타입에 @ObservedObject 당연히 사용하면 안됨.

 

SwiftUI에서 iOS17이상 타겟으로 최근에 나온 Observable를 사용하는 경우:

@Observable
class DataModel { ... }

struct ParentView: View {
    
    private let model = DataModel()

    var body: some View {
        ChildView(model: model)
    }
}

SwiftUI는 body 안에서 사용되는 Observable 객체를 자동으로 추적한다.

 

이 경우 @ObservedObject로 감싸게 되면? -> 🤬

@ObservedObject는 ObservableObject프로토콜을 요구하기 때문이다.

 

그래서 Observable사용해서 바인딩 필요하면 

@ObservedObject대신에 @Bindable 사용해야 한다.

 


 

@StateObject 와 @ObservedObject 차이

해당 예시 상황은 쉽게 접할 수 있기 때문에 요약하면,

 

@StateObject는 View가 객체를 처음 생성하고 Source of Truth로 소유하며,

View가 완전히 사라지지 않는 한 유지된다.

즉, 이 View에서 데이터를 처음 만들고 유지해야 할 때 사용한다.

 

@ObservedObject는 객체를 소유하지 않고 참조만 하며,

외부에서 주입된 상태를 잠시 관찰하거나 공유할 때 사용한다.

따라서 View가 다시 그려지면 새 인스턴스가 오면 상태는 초기화될 수 있다.


 

@EnvironmentObject

상위 뷰가 제공하는 ObservableObject를 하위 뷰 전반에서 공유하기 위한 프로퍼티 래퍼

https://developer.apple.com/documentation/swiftui/environmentobject

 

EnvironmentObject | Apple Developer Documentation

A property wrapper type for an observable object that a parent or ancestor view supplies.

developer.apple.com

 

앱 전역에서 공유 되어야 하는 상태를 전달할 때 사용한다.

상태를 담당하는 객체를 직접 전달하지 않고도 공유할 수 있게 해준다.

 

1. ObservableObject를 준수하는 객체를 상위 뷰에서 environmentObject(_:)를 통해 주입한다.

2. 하위 뷰에서는 해당 객체를 @EnvironmentObject var 선언하여 사용한다.

 

사용예시

struct ContentView: View {
    @EnvironmentObject private var settings: AppSettings

    var body: some View {
        Toggle("Dark Mode", isOn: $settings.isDarkMode)
    }
}

 

 

주입 위치가 View 트리의 실제 최상단이 아닐 경우 런타임 크래시를 유발할 수 있어

App 진입점에서 최상단에 주입하는 것이 가장 안전하다.

 


@Environment

View의 environment에 저장된 값을 읽기 전용(read-only)으로 가져오기 위한 프로퍼티 래퍼
 

Environment | Apple Developer Documentation

A property wrapper that reads a value from a view’s environment.

developer.apple.com

 

@Environment(\.colorScheme) var colorScheme: ColorScheme

EnvironmentValues의 Key path를 통해 값을 읽는다.

 

이렇게 읽은 값은

일반 프로퍼티처럼 wrappedValue로 접근한다.

if colorScheme == .dark {
    DarkContent()
} else {
    LightContent()
}

 

그래서 동작 흐름을 살펴보면,

 

1. 시스템 설정이 변경되면

2. SwiftUI가 environment 값을 업데이트하고,

3. 해당 값을 사용하는 View만 다시 렌더링한다.

 

기본적으로

@Environment로 값을 설정할 수 없다. -> 읽기만 가능

 

설정이나 재정의하고 싶다면,

environment(_:_:)  modifier를 통해 설정 또는 재정의할 수 있다.

SomeView()
    .environment(\.colorScheme, .dark)

 

 

SwiftUI 기본 environment 값 목록

https://developer.apple.com/documentation/swiftui/environmentvalues

 

EnvironmentValues | Apple Developer Documentation

A collection of environment values propagated through a view hierarchy.

developer.apple.com

 

 

Custom environment value 생성 -> Entry 매크로 사용하자.

기존 Key 기반 보일러플레이트를 대체하는 매크로

https://developer.apple.com/documentation/swiftui/entry()

 

Entry() | Apple Developer Documentation

Creates an environment values, transaction, container values, or focused values entry.

developer.apple.com

 

@Entry는 SwiftUI 내부에서 쓰이던 

EnvironmentKey, TransactionKey 같은 보일러플레이트를
자동으로 생성해주는 매크로다.

 

@Entry는 선언 위치에 따라

자동으로 다음을 생성해준다.

 

  • Key 타입 (Environment, Transaction, Container, Focused )
  • getter / setter 접근자
  • 기본값 처리

 

예전 방식은..

struct MyKey: EnvironmentKey {
    static let defaultValue = "Default"
}

extension EnvironmentValues {
    var myValue: String {
        get { self[MyKey.self] }
        set { self[MyKey.self] = newValue }
    }
}

 

 

@Entry 방식

손 쉽게 변수 선언만 하면 끝

extension EnvironmentValues {
    @Entry var myValue: String = "Default"
}

 

 

 

Observable 객체를 Environment에서 가져오기

Observable 프로토콜을 따르는 객체도 environment에서 읽을 수 있다.

 

객체는 당연히 Observable 프로토콜을 채택해야 하고,

App 또는 상위 View에서 environment에 먼저 저장되어 있어야 함

 

저장 방식은 두 가지가 있다.

- 객체 자체로 저장

- KeyPath로 저장

 

1. 객체 자체로 저장

.environment(library)

객체의 타입 자체가 Key 역할을 한다.

 

@Environment(Library.self) private var library

타입기반으로 읽는다.

 

기본적으로는 non-optional 반환이 되고,

environment에 객체가 없으면 -> 런타임 에러 발생할 수 있다.

 

 

@Environment(Library.self) private var library: Library?

그래서 안전하게 읽기 위해 옵셔널로 할 수 있음

혹여나 없어도 nil로 처리되어서 에러 발생하지 않음.

 

 

 

2. KeyPath로 저장

.environment(\.library, library)

명시적으로 Key path 사용할 수 있다.

 

@Environment(\.library) private var library

 

 


 

 

마지막으로

대부분의 프로퍼티 래퍼들은 언제 private으로 써야 할까?

https://stackoverflow.com/questions/78883671/when-should-environment-var-be-private-in-swiftui

 

When should @Environment var be private in SwiftUI?

@Environment(MyEnvironmentObj.self) var myVar vs @Environment(MyEnvironmentObj.self) private var myVar I've seen that Apple uses the former in their recent tutorials, but I use private in my code, ...

stackoverflow.com

 

 

varprivate var 

두 방식 중 무엇이 더 적절한지 고민하게 된다.

 

위의 Apple의 문서에서는 간혹 private없이 사용하는 경우도 보이지만,

실제로는 private로 선언하는 것이 더 안전한 선택인 경우가 많다.

 

Apple의 공식문서에서는 @State에 대해 다음과 같이 말한다.

Declare state as private to prevent setting it in a memberwise initializer,
which can conflict with the storage management that SwiftUI provides.

 

private로 두지 않으면,

Swift가 자동 생성하는 멤버와이즈 초기화를 통해

외부에서 실수로 값을 주입할 수 있어서 충돌을 일으킬 수 있다고 말한다.

 

대부분의 프로퍼티 래퍼들 중에 이해를 돕고자

@Enviroment를 예시로 들면 @State보다는 덜 위험하긴 하다.

왜냐하면 단순한 값이 아니라, Environment<T> 타입을 받기 때문이다.

struct Foo: View {
    @Environment(\.isEnabled) var enabled

    // 다음과 같은 멤버와이즈 이니셜라이저가 생성된다
    init(enabled: Environment<Bool> = Environment(\.isEnabled)) {
        _enabled = enabled
    }

    var body: some View {
        ...
    }
}

 

위의 코드처럼 @Environment 값은 직접 값을 전달할 수는 없다.

다만 Environment<Bool> 타입 자체를 전달하는 것은 가능하기 때문에,

의도하지 않게 다른 EnvironmentKey를 주입하면

View가 잘못된 환경 값을 참조하는 상황이 발생할 수 있다.

 

결국 View가 기대하는 값과 다른 환경을 읽게 될 가능성이 존재한다.

물론 이러한 상황이 실제로 발생할 확률은 매우 낮다.

 

그럼에도 @Environmentprivate로 선언하면

이와 같은 가능성을 사전에 차단할 수 있고,

해당 View가 어떤 환경 값에 의존하는지를 명확히 드러낼 수 있다.

 

물론 이런 문제가 발생할 가능성 자체가 이미 극히 낮기 때문에

모든 경우에 private를 강제할 필요는 없다는 주장도 충분히 이해된다.

다만 나는 아주 드문 가능성이라도 제거하는 편이 더 안정적이라고 느껴,

기본적으로 private를 사용하는 방식을 선택하려 한다.