[SwiftUI] 헷깔리는 State, Binding, ObservedObject, EnvironmentObject 총정리

SwiftUI에서의 Single Source of Truth(SSOT, 단일 진실 공급원)란 데이터의 일관성과 정확성을 유지하기 위한 중요한 개념이다.
정보 시스템에 대한 SSOT(Single Source Of Truth) 아키텍처 또는 SPOT(Single Point Of Truth) 아키텍처는 모든 데이터 요소가 마스터(또는 편집)되도록 정보 및 모델 관련 데이터 스키마를 구성하는 관행이다. 한 곳에서만 정규 형식으로 데이터 정규화를 제공한다. 이 데이터 요소에 대한 모든 가능한 연결은 참조용이다. 데이터의 다른 모든 위치는 "source of truth" 위치를 다시 참조하기 때문에 기본 위치의 데이터 요소에 대한 업데이트는 전체 시스템에 전파되어 효율성/생산성 향상, 잘못된 불일치의 쉬운 방지 같은 여러 이점을 동시에 제공한다. SSOT 아키텍처가 없으면 명확성, 생산성을 손상시켜 유지관리가 힘들어진다
SwiftUI 앱에서 사용자 인터페이스(UI)는 데이터 모델에 바인딩되어 있다. 즉, SwiftUI는 UI가 @State와 같은 데이터 모델에 바인딩되어 있어 UI는 데이터 모델의 변경에 자동으로 반응하고 변경된다. 하지만 UI를 변경하는 상태가 여러 곳에서 복사되고 변경되고 사용되면 사용자 경험(UX)의 일관성과 정확성을 유지하는 것이 어려워진다.
따라서 SwiftUI에서는 주로 @State, @Binding, @ObservedObject, @EnvironmentObject와 같은 속성 래퍼를 사용하여 데이터 모델을 관리한다.
- @State: 값 유형의 속성에 대한 저장소로 사용
- @Binding: 두 개의 뷰 간에 데이터를 전달하고 동기화하는 데 사용
- @ObservedObject: 관찰 가능한 객체를 생성
- @EnvironmentObject: 앱의 전역 상태를 나타내는 데 사용
아래 예시 코드는 https://www.youtube.com/watch?v=taoKnvqFy7k 의 코드에
내가 ObservedObject, EnvironmentObject를 추가하여 연습한 코드다!
// ContentView.swift
import SwiftUI
// 사용자 데이터를 관리하는 UserData 클래스
class UserData: ObservableObject {
@Published var name: String = ""
}
struct ContentView: View {
@State private var isDestinationPresented = false //@State를 통해서 하나의 single source of truth가 생성
@EnvironmentObject var userData: UserData // 전역적으로 공유되는 UserData 객체에 접근하기 위한 환경 객체
var body: some View {
VStack {
// 사용자 이름을 입력받는 텍스트 필드
TextField("Enter your name", text: $userData.name)
.textFieldStyle(.roundedBorder)
.padding()
// 입력된 사용자 이름을 표시하는 텍스트
Text("Hello, \(userData.name)!")
.font(.title)
Text("Click here!")
.onTapGesture {
self.isDestinationPresented.toggle()
}
.sheet(isPresented: $isDestinationPresented) {
DestinationView(isDestinationPresented: self.$isDestinationPresented)
}
}
}
}
struct DestinationView: View {
@EnvironmentObject var userData: UserData // 전역적으로 공유되는 UserData 객체에 접근하기 위한 환경 객체
@Binding var isDestinationPresented: Bool
var body: some View {
VStack {
Text("Another View")
.font(.title)
// 전역 UserData 객체의 이름을 표시하는 텍스트
Text("Hello, \(userData.name)!")
.font(.headline)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
// SourceOfTruthApp.swift
import SwiftUI
@main
struct SourceOfTruthApp: App {
@StateObject var userData = UserData() // 앱의 전역 상태로 사용되는 UserData 객체
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(userData) // ContentView에 전역 UserData 객체를 주입하여 공유
}
}
}
위의 코드는 ContentView와 DestinationView 두 개의 뷰가 있다.
UserData 클래스는 사용자 데이터를 관리하기 위한 클래스로 ObservableObject 프로토콜을 채택하고, @Published 속성 래퍼를 사용하여 name 속성을 감시 가능하게 만든다.
ContentView는 사용자로부터 이름을 입력받는 텍스트 필드와 입력된 이름을 표시하는 텍스트가 포함된 뷰다. @EnvironmentObject 속성 래퍼를 사용하여 전역적으로 공유되는 UserData 객체에 접근하며, 사용자가 이름을 입력하면 해당 객체의 name 속성이 실시간으로 업데이트된다. (영상 참고!)
DestinationView는 다른 뷰로 이동하는 데모용 뷰로, UserData 객체의 name 속성을 표시한다. @EnvironmentObject 속성 래퍼를 사용하여 전역적으로 공유되는 UserData 객체에 접근한다.
SourceOfTruthApp은 앱의 진입점으로, @StateObject 속성 래퍼를 사용하여 전역적으로 사용되는 UserData 객체를 생성하고, ContentView에 environmentObject를 사용하여 UserData 객체를 주입하여 뷰 계층 구조에서 공유될 수 있도록 한다.
꼭 ContentView 를 감싸는 struct 내에서 StateObject로 객체를 선언하고, .environmentObject(userData) 를 붙여서 전역 UserData 객체를 주입하여 공유할 수 있도록 해야한다. 이렇게 하면 ContentView와 DestinationView가 동일한 UserData 객체를 공유하고, name 속성을 올바르게 가져올 수 있다.