
UIKit과 다르게 SwiftUI는 UI의 상태가 데이터의 상태에 따라 결정된다.
'Source of Truth'라는 용어는, 앱의 특정 데이터에 대한 단 하나의 신뢰할 수 있는 원천을 의미한다. 데이터의 변경이 있을 때 해당 데이터를 참조하는 모든 뷰들이 자동으로 업데이트되어야 함을 보장하는 것.
예를 들어, 어떤 앱에서 사용자의 이름을 저장하고 있다고 치자. SwiftUI에서는 사용자의 이름을 저장하는 변수가 'Source of Truth'가 된다. 이 변수에 저장된 이름은 여러 뷰에서 사용될 수 있지만, 이름 데이터는 한 군데에서만 관리되어야 한다. 이렇게 하면 데이터 관리가 중복되거나 오류가 생길 가능성을 줄일 수 있다.
그리고 반드시 단일 'Source of Truth'가 존재해야 한다. 단일 'Source of Truth'가 존재해야 한다는 것의 의미는, 어떤 특정 데이터에 대해 하나의 정확하고 신뢰할 수 있는 출처가 있어야 한다는 의미. 이렇게 하면 데이터의 무결성을 유지할 수 있고, 앱 내에서 데이터가 변경될 때 그 변경이 예측 가능하고 관리하기 쉬워진다.
View에서 가져야 할 Source of Truth는 @State를 통해 활용할 수 있다. @State는 SwiftUI의 여러 Property Wrapper 중 하나인데, @State의 개념을 살펴보기 전에 먼저 Property Wrapper가 뭔지 살펴보자.
먼저, 프로퍼티는 클래스, 구조체 또는 열거형 등에 속한 값이다. Swift에서는 주로 두 가지 타입의 프로퍼티를 사용한다.
SwiftUI에서의 상태 관리는 UI가 데이터의 변경에 반응하여 자동으로 업데이트되도록 설계되어 있다. 이를 위해 SwiftUI는 여러 상태 프로퍼티 래퍼들을 제공한다. 여기서 프로퍼티 래퍼들은 데이터의 저장 및 변화 관리를 용이하게 하며, 뷰의 재생성과 직접적으로 관련이 있다.
@propertyWrapper
struct Logged<T> {
private var value: T
var wrappedValue: T {
get { value }
set {
print("Setting value to \(newValue)")
value = newValue
}
}
init(wrappedValue initialValue: T) {
self.value = initialValue
print("Initialized with \(initialValue)")
}
}
struct Example {
@Logged var count: Int
}
@State는 위에서 언급했던 프로퍼티 래퍼 중 하나이다. View에서 가져야 할 Source of Truth라고 할 수 있다. Data에 대한 상태를 저장하고 관찰한다.
View에 대한 상태를 저장하기 위한 목적으로 설계되었기 때문에, 해당 뷰가 data를 소유하고 관리한다는 개념을 명시적으로 나타내기 위해 private 접근 레벨을 사용하는 게 좋다.
Data가 변경되면 View도 함께 변경된다. 즉, view가 data에 의존성을 갖고 있다는 것을 의미한다. 변경사항이 감지될 때마다 매번 View를 재생성한다. 뷰의 재생성은 실제로 전체 뷰가 새로 그려지는 것을 의미하지 않고, SwiftUI의 뷰 렌더링 시스템이 변경된 상태에 대해 반응하여 뷰의 일부분만을 업데이트한다.
import SwiftUI
// ParentView는 @State를 사용하여 슬라이더 값 관리
struct ParentView: View {
@State private var sliderValue: Double = 0 // Source of Truth
var body: some View {
VStack {
Text("Current Value: \(sliderValue, specifier: "%.1f")")
ChildView(sliderValue: $sliderValue)
}
.padding()
}
}
// ChildView는 ParentView에서 제공하는 @Binding을 통해 슬라이더 값에 대한 바인딩을 가짐
struct ChildView: View {
@Binding var sliderValue: Double // Source of Truth를 참조
var body: some View {
Slider(value: $sliderValue, in: 0...100)
.padding()
}
}
struct ContentView2: View {
var body: some View {
ParentView()
}
}
#Preview {
ContentView2()
}
SwiftUI처럼 이렇게 중앙에서 데이터를 관리하는 것은 특히 큰 프로젝트나 여러 데이터가 상호작용하는 복잡한 상황에서 매우 유용하다. 데이터의 출처가 명확하고 하나이기 때문에 버그를 찾기 쉽고, 데이터를 업데이트하거나 수정할 때 다른 부분에 미치는 영향을 쉽게 예측할 수 있다.