| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
- Firestore
- nidthirdpartylogin
- swiftdata
- github 시작하기
- 타뷸레이션
- TestFlight
- 클린 아키텍처
- 팀 개발을 위한 git
- heic
- JPEG
- 함께자라기
- contentalignmentpoint
- .pbxproj
- png
- 코드스쿼드
- xcode 공백 표시
- SwiftUI
- 무한스크롤
- 테스트 타겟
- NSTextStorageDelegate
- swift 모듈화
- 캐러셀
- xcode 엔터 표시
- Cocoa Pod
- Tuist
- spm 에러
- JPG
- webp
- NSTextStorage
- fetchdescriptor
- Today
- Total
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

var 와 private 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가 기대하는 값과 다른 환경을 읽게 될 가능성이 존재한다.
물론 이러한 상황이 실제로 발생할 확률은 매우 낮다.
그럼에도 @Environment를 private로 선언하면
이와 같은 가능성을 사전에 차단할 수 있고,
해당 View가 어떤 환경 값에 의존하는지를 명확히 드러낼 수 있다.
물론 이런 문제가 발생할 가능성 자체가 이미 극히 낮기 때문에
모든 경우에 private를 강제할 필요는 없다는 주장도 충분히 이해된다.
다만 나는 아주 드문 가능성이라도 제거하는 편이 더 안정적이라고 느껴,
기본적으로 private를 사용하는 방식을 선택하려 한다.
'💻' 카테고리의 다른 글
| @State, @Binding (프로퍼티 래퍼 - 2 ) (0) | 2025.12.24 |
|---|---|
| @propertyWrapper 프로퍼티 래퍼란? - 1 (0) | 2025.12.22 |
| SwiftUI ) @Published는 어떻게 동작하는가 - objectWillChange (1) | 2025.11.27 |
| UITextView에서 한글 스타일 적용 (2) | 2025.10.13 |
| async let vs TaskGroup vs 연속 await (2) | 2025.08.28 |