SwiftUI에서 뷰 간에 데이터를 주고받을 때 @State, @Binding, 그리고 $ 기호를 사용한다.
각각이 무엇인지, 왜 필요한지, 내부적으로 어떻게 연결되는지를 순서대로 정리한다.
1. Source of Truth — 데이터는 한 곳에서만 소유한다
SwiftUI의 데이터 흐름은 Source of Truth 원칙 위에서 동작한다.
Source of Truth란 "어떤 상태가 딱 한 곳에서만 관리되고, 나머지는 그걸 참조한다"는 원칙이다.
Source of Truth가 여러 곳에 흩어지면 어떤 문제가 생기는지 먼저 보자.
// ❌ 안티패턴 — 같은 데이터를 두 곳에서 따로 관리
struct ParentView: View {
@State var isOn = false
}
struct ChildView: View {
@State var isOn = false // 부모와 별개의 복사본 → 동기화 문제 발생
}
ParentView와 ChildView가 각자 isOn을 들고 있으면, 둘 중 하나가 바뀌어도 나머지는 모른다.
UI가 꼬이는 원인이 된다.
SwiftUI가 제시하는 해법은 단순하다.
데이터는 한 곳에서만 소유하고, 나머지는 그 원본을 참조한다.
2. @State: 뷰가 소유하는 Source of Truth
@State는 뷰가 직접 소유하는 상태다.
struct CounterView: View {
@State private var count = 0 // 이 뷰의 Source of Truth
var body: some View {
Button("탭: \(count)") {
count += 1
}
}
}
@State로 선언된 값이 바뀌면 SwiftUI는 해당 뷰의 body를 다시 호출해서 화면을 갱신한다.
한 가지 주의할 점이 있다. SwiftUI의 뷰는 struct이기 때문에, 상태를 뷰 안에 단순히 var로 선언하면 struct가 새로 만들어질 때 값이 초기화된다.
@State는 이 값을 SwiftUI 내부 저장소에 따로 보관해서, 뷰가 다시 생성되더라도 값이 유지된다.
// ❌ 그냥 var — 뷰가 다시 만들어지면 값이 초기화됨
var count = 0
// ✅ @State — SwiftUI 내부 저장소에 보관, 값이 유지됨
@State private var count = 0
3. Property Wrapper의 세 가지 접근법
@State는 Property Wrapper다. Property Wrapper는 값을 세 가지 방식으로 노출한다.
@State private var isOn = false
isOn // wrappedValue — 실제 값 (Bool)
$isOn // projectedValue — 추가로 제공하는 값 (Binding<Bool>)
_isOn // 래퍼 인스턴스 자체 (State<Bool>)
- wrappedValue: 실제 값
- Property Wrapper가 감싸고 있는 실제 값
- 값을 읽고 쓸 때
- projectedValue(Binding)
- Property Wrapper가 추가로 제공하는 값
- @State의 경우 Binding<Bool>을 반환
- 값의 복사본이 아닌 원본을 가리키는 참조
- 자식이 바꾸면 부모의 @State가 바뀜
- 값을 참조로 넘길 때
- 래퍼 인스턴스 자체 (State<Bool>)
- Property Wrapper 객체 자체에 접근
- 타입은 State<Bool>
- 주로 초기화(init) 할 때 사용
- init에서 래퍼 자체를 초기화할 때
isOn = true로 쓰면 wrappedValue에 접근하는 것이지만, _isOn = State(initialValue: true)는 래퍼 인스턴스 자체를 교체
struct CounterView: View {
@State private var isOn: Bool
// 외부에서 초기값을 주입하고 싶을 때
init(initialValue: Bool) {
_isOn = State(initialValue: initialValue) // 래퍼 자체를 초기화
}
}
4. Projected Value: $가 하는 일
$isOn은 @State의 Projected Value다.
$를 붙이면 Property Wrapper가 추가로 제공하는 값에 접근할 수 있다.
@State의 Projected Value는 Binding<Bool>이다.
@State private var isOn = false
print(type(of: isOn)) // Bool
print(type(of: $isOn)) // Binding<Bool>
Binding은 값의 복사본이 아니라 원본을 가리키는 참조다.
자식 뷰에 $isOn을 넘기면, 자식이 그 값을 바꿀 때 부모의 @State가 직접 바뀐다.
Projected Value가 뭘 제공할지는 Property Wrapper가 직접 결정한다. @State는 Binding을 제공하지만, 다른 래퍼는 다른 타입을 제공한다.
| Property Wrapper | wrappedValue | projectedValue ($) |
| @State | Bool | Binding<Bool> |
| @StateObject | ViewModel | ObservedObject<ViewModel>.Wrapper |
| @ObservedObject | ViewModel | ObservedObject<ViewModel>.Wrapper |
| @Published | Int | Publisher<Int, Never> |
| @FocusState | Bool | FocusState<Bool>.Binding |
5. @Binding — 원본을 참조하는 뷰
@Binding은 Source of Truth를 소유하지 않는다.
다른 곳에 있는 @State의 원본을 참조할 뿐이다.
struct ToggleView: View {
@Binding var isOn: Bool // 원본은 부모의 @State
var body: some View {
Toggle("알림", isOn: $isOn)
}
}
@Binding으로 받은 값을 바꾸면, 원본인 부모의 @State가 바뀌고, SwiftUI가 관련된 뷰를 모두 다시 렌더링한다.
6. 전체 흐름
struct ParentView: View {
@State private var isOn = false // ✅ Source of Truth는 여기 하나
var body: some View {
VStack {
Text(isOn ? "켜짐" : "꺼짐") // wrappedValue 읽기
ChildView(isOn: $isOn) // projectedValue(Binding) 전달
}
}
}
struct ChildView: View {
@Binding var isOn: Bool // 원본 참조, 소유하지 않음
var body: some View {
Toggle("스위치", isOn: $isOn) // @Binding의 projectedValue
}
}
데이터 흐름을 정리하면 이렇다.
ParentView
@State var isOn = false ← Source of Truth (원본 소유)
│
│ $isOn (Binding<Bool> 전달)
▼
ChildView
@Binding var isOn ← 원본 참조
│
│ 값 변경
▼
ParentView의 @State 직접 변경 → 관련 뷰 전체 리렌더링
7. 왜 값을 그냥 넘기면 안 되는가?
struct는 값 타입이기 때문에, 전달하면 복사본이 생긴다.
// ❌ 값(복사본)을 넘기는 경우
ChildView(isOn: isOn)
// ChildView가 받은 건 복사본 → 바꿔도 부모의 @State에 영향 없음
// ✅ Binding(참조)을 넘기는 경우
ChildView(isOn: $isOn)
// ChildView가 받은 건 원본에 대한 참조 → 바꾸면 부모의 @State가 바뀜
이것이 $가 필요한 이유다.
정리
| 개념 | 역할 |
| Source of Truth | 데이터를 한 곳에서만 소유한다는 원칙 |
| @State | 뷰가 직접 소유하는 Source of Truth |
| wrappedValue | @State의 실제 값 (isOn) |
| projectedValue | Property Wrapper가 추가로 제공하는 값 ($isOn) |
| @State의 Projected Value | Binding<Bool> — 원본에 대한 참조 |
| @Binding | 원본을 소유하지 않고 참조만 하는 뷰의 상태 |
SwiftUI의 데이터 흐름은 결국 하나의 원칙으로 귀결된다.
데이터는 한 곳에서 소유하고 (@State), 나머지는 참조한다 (@Binding)
그 참조를 가능하게 하는 것이 Projected Value($)이다.
'iOS > Swift' 카테고리의 다른 글
| [Swift] borrowing, consuming (1) | 2026.03.15 |
|---|---|
| [Swift] KeyPath (0) | 2026.03.08 |
| [Combine] throttle와 debounce (0) | 2025.05.20 |
| [iOS] ViewLife Cycle (UIKit vs SwiftUI) (0) | 2024.05.20 |
| [CS] 동기(Sync), 비동기(Async) (2) | 2024.02.16 |