--- id: ios-swiftui-state-property-wrappers title: SwiftUI State Property Wrappers — 어떤 걸 언제 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [ios, swiftui, state, property-wrapper, vibe-coding] tech_stack: { language: "Swift 5.7+ / SwiftUI 4+", applicable_to: ["iOS 16+", "macOS 13+"] } applied_in: [] aliases: [@State, @Binding, @StateObject, @ObservedObject, @EnvironmentObject, @Observable] --- # SwiftUI State Property Wrappers > 잘못 고르면 view identity 문제 + 의도치 않은 재생성. 핵심: **소유자 = `@State` / `@StateObject` / `@Observable`**, **차용자 = `@Binding` / `@ObservedObject` / `Bindable`**, **트리 전역 = `@Environment(Object)`**. ## 📖 핵심 개념 - iOS 17+ 권장: `@Observable` 매크로 + `let`/`var` 일반 프로퍼티. 정밀 추적. - iOS 16-: `ObservableObject` + `@Published` + `@StateObject` / `@ObservedObject`. ## 💻 코드 패턴 ### iOS 17+ — @Observable 권장 ```swift @Observable final class CartModel { var items: [Item] = [] var total: Decimal = 0 } struct CartView: View { @State private var cart = CartModel() // 소유자 var body: some View { Text("\(cart.total)") ChildView(cart: cart) // 그냥 전달 } } struct ChildView: View { let cart: CartModel // 차용자 — 별도 wrapper 불필요 (Observation 자동) @Bindable var cartBindable: CartModel // 양방향 binding 필요시 var body: some View { TextField("name", text: $cartBindable.note) } } ``` ### iOS 16- — ObservableObject ```swift final class CartModel: ObservableObject { @Published var items: [Item] = [] } struct CartView: View { @StateObject private var cart = CartModel() // 소유자 var body: some View { ChildView(cart: cart) } } struct ChildView: View { @ObservedObject var cart: CartModel // 차용자 } ``` ### Environment 주입 ```swift struct AppView: View { @State private var user = UserModel() var body: some View { ContentView() .environment(user) // iOS 17+ // .environmentObject(user) // iOS 16- } } struct DeepChild: View { @Environment(UserModel.self) var user // iOS 17+ // @EnvironmentObject var user: UserModel // iOS 16- } ``` ### Binding 양방향 ```swift struct Toggle: View { @Binding var isOn: Bool var body: some View { Button(isOn ? "on" : "off") { isOn.toggle() } } } // 호출 @State var on = false Toggle(isOn: $on) ``` ## 🤔 의사결정 기준 | 역할 | iOS 17+ | iOS 16- | |---|---|---| | 값 타입 (Bool, String, struct) 소유 | `@State` | `@State` | | 클래스 모델 소유 | `@State` (Observable) | `@StateObject` | | 클래스 모델 차용 | 일반 let | `@ObservedObject` | | 양방향 binding 차용 | `@Bindable` | `@Binding` (값 타입만) | | 트리 전역 | `@Environment(Type.self)` | `@EnvironmentObject` | ## ❌ 안티패턴 - **부모-자식 모두 `@StateObject`**: 자식 view 가 재생성되면 새 instance. 자식은 `@ObservedObject` 또는 받기. - **`@StateObject var x = makeBigThing()`**: init 매번 호출 (실제 사용 안 됨). 가벼운 init 권장. - **`@ObservedObject` 를 owner 처럼 사용**: parent rebuild 시 자식 instance 새로 생성 → state 잃음. - **iOS 17+ 코드에 `@Published`, `ObservableObject` 혼재**: 일관성. 한 모델은 한 패턴. - **EnvironmentObject 주입 안 하고 사용**: 런타임 crash. 모델 매크로 / preview 에서도 주입. - **값 타입 큰 struct 를 @State 로 자주 mutate**: copy 비용. 클래스 + Observable. ## 🤖 LLM 활용 힌트 - iOS 17+ vs 16- 명시 후 코드 작성. - Preview 에서도 EnvironmentObject 주입 코드 같이. ## 🔗 관련 문서 - [[iOS_SwiftUI_Lifecycle_View_Identity]] - [[iOS_Swift_Concurrency_async_await]]