---
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]]
