본문 바로가기
iOS/Swift

[SwiftUI] @State, @Binding, Projected Value

by Bokoo14 2026. 3. 8.

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