@State, @Binding, @StateObject, @ObservedObject, @EnvironmentObject
SwiftUI로 앱을 만들다 보면 “상태 관리”가 정말 중요하다는 걸 느끼게 된다.
값이 바뀔 때마다 UI가 자동으로 업데이트되니 편리하지만, 상태의 소유권과 생명주기를 제대로 이해하지 않으면 예상치 못한 버그를 맞이할 수 있다.
그럼 하나씩 정리해보자.
@State — 로컬 상태@State는 뷰 내부에서만 관리되는 로컬 상태다.
값 타입(Int, String, struct)에 적합하며, 값이 바뀌면 뷰가 자동으로 다시 그려진다.
“이걸 어디에 써야 하지?”
→ 뷰 내부에서만 쓰는 값일 때 사용하면 된다. 외부와 공유할 필요가 없을 때 가장 적합하다.
struct CounterView: View {
@State private var count = 0 // 로컬 상태
var body: some View {
VStack {
Text("Count: \(count)")
Button("증가") {
count += 1 // 값 변경 → UI 자동 업데이트
}
}
}
}
@Binding — 상위 상태를 하위에서 수정@Binding은 상위 뷰의 @State 값을 하위 뷰에서 읽고 수정할 수 있게 연결해 준다.
“하위 뷰가 상태를 직접 소유하면 안 되나?”
→ 하위에서 소유하면 상위와 값이 따로 놀게 된다. 상위 상태를 공유해야 한다면 Binding이 정답이다.
struct ParentView: View {
@State private var name = "홍길동"
var body: some View {
VStack {
Text("이름: \(name)")
ChildView(name: $name) // Binding으로 전달
}
}
}
struct ChildView: View {
@Binding var name: String // 상위의 상태에 직접 연결
var body: some View {
TextField("이름 입력", text: $name)
.textFieldStyle(.roundedBorder)
}
}
하위에서 이름을 바꾸면 부모 뷰의 name도 즉시 바뀐다.
@StateObject — 참조 타입 상태의 소유자@StateObject는 뷰가 직접 ObservableObject를 소유하고 관리할 때 사용한다.
SwiftUI가 객체의 생명주기를 관리하며, 값이 바뀌면 뷰가 다시 그려진다.
“왜 그냥 @State로 클래스 타입을 쓰면 안 돼?”
→ @State는 값 타입 전용이다. 클래스 같은 참조 타입은 @StateObject를 써야 한다.
class UserSettings: ObservableObject {
@Published var score = 0
}
struct StateObjectExample: View {
@StateObject private var settings = UserSettings() // 뷰가 소유
var body: some View {
VStack {
Text("점수: \(settings.score)")
Button("점수 증가") {
settings.score += 1
}
}
}
}
@StateObject는 뷰가 새로 그려져도 기존 객체를 유지한다.
@ObservedObject — 다른 뷰에서 만든 객체 관찰@ObservedObject는 상위에서 이미 만든 ObservableObject를 관찰만 할 때 사용한다.
뷰는 이 객체를 소유하지 않는다.
“그럼 @StateObject랑 뭐가 다른데?”
→ 소유권의 차이다.
@StateObject: 내가 소유 → 내가 생성@ObservedObject: 남이 소유 → 나는 관찰만struct ParentView: View {
@StateObject private var settings = UserSettings() // 부모가 소유
var body: some View {
VStack {
Text("부모 점수: \(settings.score)")
ChildView(settings: settings)
}
}
}
struct ChildView: View {
@ObservedObject var settings: UserSettings // 단순 관찰
var body: some View {
VStack {
Text("자식 점수: \(settings.score)")
Button("점수 증가") {
settings.score += 1
}
}
}
}
부모와 자식이 같은 객체를 공유하며, 값이 바뀌면 둘 다 업데이트된다.
@EnvironmentObject — 전역 상태 공유@EnvironmentObject는 앱 전역에서 공유하는 상태를 주입받을 때 사용한다.
상위에서 .environmentObject(_:)로 한 번만 넣어주면 하위 뷰 어디서든 접근 가능하다.
“그냥 파라미터로 넘기면 되지 않나?”
→ 뷰 계층이 깊어질수록 파라미터로 계속 넘기는 게 귀찮아진다.
@EnvironmentObject를 쓰면 이 문제를 깔끔하게 해결할 수 있다.
class AppSettings: ObservableObject {
@Published var themeColor: Color = .blue
}
@main
struct MyApp: App {
@StateObject private var settings = AppSettings()
var body: some Scene {
WindowGroup {
RootView()
.environmentObject(settings) // 전역 주입
}
}
}
struct RootView: View {
var body: some View {
ChildView() // 별도 파라미터 필요 없음
}
}
struct ChildView: View {
@EnvironmentObject var settings: AppSettings
var body: some View {
VStack {
Text("테마 색상")
.foregroundColor(settings.themeColor)
Button("색상 변경") {
settings.themeColor = .red
}
}
}
}
단, 주입하지 않으면 런타임 에러가 난다.
| 래퍼 | 소유권 | 대상 | 사용 위치 | 용도 |
|---|---|---|---|---|
| @State | 뷰가 소유 | 값 타입 | 로컬 | 뷰 내부 상태 |
| @Binding | 상위가 소유 | 값 타입 | 하위 | 상위 상태 수정 |
| @StateObject | 뷰가 소유 | 참조 타입 | 초기 생성 | 뷰가 직접 관리 |
| @ObservedObject | 상위가 소유 | 참조 타입 | 하위 | 상위 객체 관찰 |
| @EnvironmentObject | 전역 | 참조 타입 | 전역 하위 뷰 | 전역 상태 공유 |
@State / @StateObject@Binding / @ObservedObject@EnvironmentObject