SwiftUI에서는 데이터가 변경될 때 마다 자동으로 UI를 업데이트가 되는데요. 이게 가능한 이유가 바로 프로퍼티 래퍼들 덕분입니다.
프로퍼티 래퍼를 잘 활용하기 위해서 오늘은 SwiftUI에서 사용하는 상태관리, 데이터 흐름 관리에 필요한 프로퍼티 래퍼들을 알아보겠습니다.
먼저 공식 문서들을 살펴보면 "Source of Truth" 라는 말을 계속 이야기하고있습니다. 이 구문은 신뢰할 수 있는 데이터 즉 데이터는 여러곳에서 혼동되거나 중복하여 소유하고 사용하면 안되고, 한 곳에서 소유하고 관리되어야한다는 뜻입니다.
이 "Source of Truth"라는 원칙을 지키기위해 설계된 것이 바로 SwiftUI의 프로퍼티 래퍼들인 것입니다.
@State
@State는 하나의 뷰 안에서만 사용하는 단일 값(Int, String, Bool 등)을 저장하기위해 사용합니다. @State가 선언된 뷰가 그 상태를 소유하고있습니다. @State를 사용하면 값이 변화되는 것을 계속 감지하고있기 때문에 값이 변경되었을 때 UI를 다시 그려줄 수 있습니다. 그리고 뷰와 생명주기가 같기 때문에 뷰가 없어지면 @State도 같이 없어지게 됩니다.
@Binding
@Binding은 원본 데이터를 빌려서 사용하고 수정할 수 있게 해주는 프로퍼티 래퍼 입니다. 데이터에 대한 소유권은 없으며, 부모 뷰에서 소유하고 있는 데이터를 자식뷰에서 변경하고자할 때 사용합니다.
// 자식 뷰: 스위치 컨트롤
struct LightSwitchControl: View {
@Binding var isOn: Bool // 소유권 없이 연결만 받음
var body: some View {
Toggle("전등", isOn: $isOn)
}
}
// 부모 뷰: 방
struct RoomView: View {
@State private var isLightOn: Bool = false // isLightOn 상태를 직접 소유
var body: some View {
VStack {
Image(systemName: isLightOn ? "lightbulb.fill" : "lightbulb.slash")
.font(.largeTitle)
.foregroundColor(isLightOn ? .yellow : .gray)
// 자식 뷰에게 $를 붙여 바인딩 전달
LightSwitchControl(isOn: $isLightOn)
.padding()
}
}
}
@Published
@Published는 ObservableObject를 따르는 클래스의 내부에서 사용됩니다. 이 프로퍼티 래퍼가 붙은 변수의 값이 변하면 그 값이 할당되기 직전에 변경 이벤트를 발생시킵니다. 주로 @StateObject나 @ObservedObject가 이벤트를 구독하고 뷰를 업데이트하는 역할을 수행합니다.
@StateObject
@StateObject는 복잡한 참조타입의 객체를 뷰가 직접 생성하고 소유할 때 사용합니다. @State와 비슷한 역할을 하지만 @State는 비교적 간단한 값들을 소유하고, @StateObject는 MVVM에서 ViewModel과 같은 복잡한 객체를 소유합니다. 그리고 @StateObject는 @State와 달리 뷰와 생명주기를 같이하지않기 때문에 뷰가 다시 그려지더라도 @StateObject는 없어지지않고 생명주기를 유지합니다.
@ObservedObject
@ObservedObject는 객체를 소유하지않고 외부에서 만들어진 ObservableObject객체를 관찰하고 @Published의 변경 이벤트를 받으면 뷰를 업데이트 합니다. @StateObject와의 차이는 @ObservedObject는 객체를 소유하지않고 전달 받아서 관찰만 할 수 있다는 점이 차이점입니다.
// 데이터와 로직을 가진 ViewModel
class GameViewModel: ObservableObject {
@Published var score = 0 // 이 값이 바뀌면 자동으로 변경 이벤트 방출
}
// 자식 뷰: 점수판
struct ScoreDisplayView: View {
@ObservedObject var viewModel: GameViewModel // 객체를 관찰만 함
var body: some View {
Text("Score: \(viewModel.score)")
.padding()
.background(.yellow)
}
}
// 부모 뷰: 게임 화면
struct GameView: View {
@StateObject private var viewModel = GameViewModel() // ViewModel을 직접 생성하고 소유
var body: some View {
VStack(spacing: 20) {
ScoreDisplayView(viewModel: viewModel) // 자식에게 객체를 전달
Button("점수 획득!") {
viewModel.score += 10
}
}
}
}
@EnvironmentObject
@EnvironmentObject는 앱의모든 뷰가 함께 공유해야하는 데이터를 관리할 때 사용합니다. 예를 들면 로그인 상태, 앱의 테마 설정 등이 @EnvironmentObject를 사용해야하는 데이터입니다.
상위 뷰에서 .environmentObject()를 통해 객체를 등록해두면 하위 자식 뷰에서 따로 전달하거나 받을 필요없이 @EnvironmentObject를 통해서 바로 사용할 수 있습니다.
하지만 .environmentObject()로 주입할 수 있는 객체는 ObservableObject 프로토콜을 따르는 객체만 주입할 수 있습니다. 왜냐하면 이 프로퍼티 래퍼는 여러 뷰들이 함께 관찰하고 사용하는 객체를 제공하는 것이기 때문에 객체의 데이터가 변화한다면 감지할 수 있어야합니다. 그렇기 때문에 ObservableObject 프로토콜을 따르는 객체만 주입할 수 있습니다.
func environmentObject<T>(_ object: T) -> some View where T : ObservableObject
공식 문서에 있는 .environmentObject()함수 정의에도 ObservableObject를 따라야한다고 명시되어있습니다.
@main
struct MyApp: App {
@StateObject private var settings = UserSettings()
var body: some Scene {
WindowGroup {
GrandParentView()
.environmentObject(settings)
}
}
}
// 자식 뷰
struct ChildView: View {
@EnvironmentObject var settings: UserSettings
var body: some View {
// 부모로부터 직접 전달받지 않았지만, 값을 사용할 수 있습니다.
Text("점수: \(settings.theme)")
}
}
'iOS' 카테고리의 다른 글
| 클로저 캡쳐가 일어날 때 ARC는 어떻게 변할까? (0) | 2025.08.19 |
|---|---|
| 함수 인자로 class를 받는다면 ARC의 변화가 있을까? (1) | 2025.08.18 |
| SwiftData (2) | 2025.08.13 |
| Coordinator 패턴으로 SwiftUI 화면 전환 시키기 (1) | 2025.08.12 |
| SwiftUI의 ViewModifier (0) | 2025.08.11 |