이글을 작성하는 이유는 SwiftUI에서 많이 사용되는 "상태(State)"와 "Observation"의 개념을 명확히 이해하기 위해 WWDC 2023에서 소개된 Discover Observation in SwiftUI에 대한 정리글을 작성해보겠습니다.
내용을 정리하기전 Observation과 상태라는 용어가 자주 등장 하는데, 대체 Observation과 상태가 어떤 의미를 가지는지 간단하게 알아보고 가겠습니다.
상태
- 정의: 시간에 따라 변할 수 있는 데이터.
- 역할: UI에 반영되어 사용자에게 보여지는 데이터를 관리.
- 특징: 상태의 변화가 UI에 자동으로 반영됨.
Observation
- 정의: 상태의 변화를 감지하고 UI를 업데이트하는 메커니즘.
- 역할: 상태 변화를 자동으로 감지하여 관련된 UI를 다시 렌더링하고, 데이터와 UI를 동기화.
- 특징: SwiftUI는 상태 변화를 자동으로 감지하고 해당 뷰를 업데이트.
즉, "상태는 시간이 지나거나 사용자의 상호작용에 따라 변할 수 있는 데이터이고, Observation은 이러한 상태 변화를 감지 하고, 그에따라 UI를 업데이트 한다." 라고 생각하면 됩니다.
Observation은 프로퍼티의 변화를 추적하는 Swift의 기능입니다. 이 기능은 Swift의 매크로 시스템 덕분에 가능한 것입니다.
우리가 평소 작성하는 데이터 모델 타입을 SwiftUI에서도 사용하고 싶을 때도 있습니다.
그러면 다음과 같이 @Observable을 추가하는 것만으로도 UI가 데이터 모델의 변화에 대응할 수 있게 됩니다.
@Observable class FoodTruckModel {
var orders: [Order] = []
var donuts = Donut.all
}
@Observable은 Swift컴파일러에 명령을 내려서 작성한 코드를 확장형 Observable 타입으로 바꿉니다. 이렇게 함으로써 Observable 타입으로 SwiftUI 뷰를 작동시킬수 있게됩니다.
다른 프로퍼티 래퍼가 없어도 작동할 수 있습니다.
@Observable class FoodTruckModel {
var orders: [Order] = []
var donuts = Donut.all
}
struct DonutMenu: View {
let model: FoodTruckModel
var body: some View {
List {
Section("Donuts") {
ForEach(model.donuts) { donut in
Text(donut.name)
}
Button("Add new donut") {
model.addDonut()
}
}
}
}
}
이전처럼 @StateObject 또는 @ObservedObject와 같은 프로퍼티 래퍼를 작성하지 않고 let model: ~로 작성해도 됩니다.
이렇게 만 작성해도 뷰가 실행될 때 SwiftUI는 Observable 타입에서 사용된 프로퍼티의 모든 접근을 추적합니다. 그리고 이 추적 정보를 이용해 특정 인스턴스에서 프로퍼티에 다음 변화가 언제 일어날지를 예측하게 되는 것입니다.
위 코드를 보면 Add new donut 버튼을 클릭해 donuts 변경하면 DonutMenu 뷰가 무효화 되고 UI도 변경 사항에 맞춰 업데이트됩니다.
무효화
SwiftUI에서 뷰의 무효화(invalidation)란, 뷰의 상태가 변경되었을 때 해당 뷰를 다시 그려야 한다는 것을 의미합니다. SwiftUI는 상태(state)와 데이터가 변경되면 뷰를 다시 그리기 위해 자동으로 무효화 과정을 거칩니다.
무효화 과정은 SwiftUI가 뷰의 상태를 감지하고 변경 사항이 있을 때마다 자동으로 뷰를 다시 그리도록 하는 메커니즘을 의미합니다. 이를 통해 복잡한 뷰 갱신 로직을 작성할 필요 없이 데이터와 상태 변화에만 집중할 수 있습니다.
예를 들어, 다음과 같은 코드에서
count값이 변경되면Text뷰가 무효화되어 다시 그려집니다:struct ContentView: View { @State private var count = 0 var body: some View { VStack { Text("Count: \(count)") Button(action: { count = count + 1 }) { Text("Increment") } } } }위 코드에서 버튼을 클릭하면
count값이 증가하고,Text뷰가 무효화되어 새로운 값을 반영하기 위해 뷰를 다시 그려집니다.
그러면 모델에서 연산프로퍼티를 사용하면 어떻게 될까요?
연산 프로퍼티를 추가할 때도 같은 규칙이 적용됩니다. 프로퍼티에 변화가 생기면 UI는 업데이트 됩니다.
@Observable class FoodTruckModel {
var orders: [Order] = []
var donuts = Donut.all
var orderCount: Int { orders.count }
}
struct DonutMenu: View {
let model: FoodTruckModel
var body: some View {
List {
Section("Donuts") {
ForEach(model.donuts) { donut in
Text(donut.name)
}
Button("Add new donut") {
model.addDonut()
}
}
Section("Orders") {
LabeledContent("Count", value: "\(model.orderCount)")
}
}
}
}
model의 orderCount를 호출해서 orders 프로퍼티에 접근합니다.
즉, orders의 값이 변경되면 orderCount가 orders 프로퍼티에 접근했기 때문에 해당 텍스트가 업데이트 됩니다.
@Observable매크로를 사용 하면 타입이 확장되어서 Observation을 지원할 수 있게 됩니다.
이에따라 SwiftUI는 해당 프로퍼티에 대한 접근을 추적할 수 있고, Observation에서 다음 프로퍼티가 언제 변할 지 관찰할 수 있게 되는 것입니다.
이러한 추적이 가능해지므로 프로퍼티에 변화가 생겼을때 UI가 뷰를 다시 계산함으로써 성능이 향상됩니다.
@Observable의 이점
- Macro
- Tracks access
- Property canges casuse UI updates
Observable이 있을때 SwiftUI의 프로퍼티 래퍼 사용법을 알아 보겠습니다.
SwiftUI 핵심적인 프로퍼티 래퍼는 State, Environment, Bindable입니다.
위에서 프로퍼티 래퍼를 사용하지 않아도 되는 경우를 살펴 보았기 때문에, 이번에는 프로퍼티 래퍼를 사용하는 경우를 살펴보겠습니다.
@State는 모델 안에 뷰 전용 상태를 저장해야 할 때 사용합니다.
"모델 안에 뷰 전용 상태를 저장해야 할 때"의 의미
“뷰 전용 상태”란 특정 뷰에서만 사용되고, 다른 뷰나 모델에 영향을 미치지 않는 상태를 의미합니다. "로컬 상태"라고도 합니다.
즉, 해당 상태가 오직 특정 뷰 내에서만 사용되는 데이터라고 생각하면 됩니다.
버튼 클릭으로 카운터 값이 증가하는 코드 예시를 들어 보겠습니다.
카운터 값은 특정 버튼과 관련의 뷰의 로컬 상태이며, 다른 뷰나 모델에 영향을 미치지 않습니다.
이때
@State를 사용하여 카운터 값을 관리하고, 버튼 클릭 시 값을 업데이트 하면 뷰가 자동으로 업데이트 됩니다.struct MyView: View { @State private var count = 0 var body: some View { VStack { Text("카운터: \(count)") Button("증가") { self.count += 1 } } } }여기서
@State private var count = 0는MyView에서만 사용되는 “카운터”라는 로컬 상태를 선언합니다.
다음 코드는 Observable 모델 객체인 Donut을 시트로 표현한 모습입니다.
struct DonutListView: View {
var donutList: DonutList
@State private var donutToAdd: Donut?
var body: some View {
List(donutList.donuts) { DonutView(donut: $0) }
Button("Add Donut") { donutToAdd = Donut() }
.sheet(item: $donutToAdd) {
TextField("Name", text: $donutToAdd.name)
Button("Save") {
donutList.donuts.append(donutToAdd)
donutToAdd = nil
}
Button("Cancel") { donutToAdd = nil }
}
}
}
위 코드에서 시트를 띄우기 위해 donutToAdd 상태 변수를 사용합니다.
해당 뷰에서 자체적으로 Donut 모델의 저장된 상태를 가지고 시트를 컨트롤 해주기 위해 @State가 필요합니다. 만약 @State가 없다면 시트에 바인딩 될 수 없으므로 @State 상태 값이 필요한 것입니다.
donutToAdd 프로퍼티는 @State로 선언되었기 때문에 소속된 뷰의 수명 동안만 관리할 수 있다.
Environment를 사용하면 전역적으로 액세스 가능한 값으로 값을 전파할 수 있습니다. 이를 통해 여러 곳에서 공유할 수 있습니다
@Observable class Account {
var userName: String?
}
struct FoodTruckMenuView : View {
@Environment(Account.self) var account
var body: some View {
if let name = account.userName {
HStack { Text(name); Button("Log out") { account.logOut() } }
} else {
Button("Login") { account.showLogin() }
}
}
}
FoodTruckMenuView를 불러올 때는 acoount객체의 userName프로퍼티에 접근이 이루어집니다. userName이 바뀌면 메뉴 뷰도 업데이트 됩니다.
@Bindable은 해당 타입으로부터 바인딩 가능하도록 만들어 줍니다.
바인딩할 프로퍼티에 @Bindable을 작성하고 $ 구문을 사용해 해당 프로퍼에 대한 바인딩을 가져오면됩니다.
대부분 Observable 타입에 대한 바인딩일 것 입니다.
그러면 다음과 같이 사용할 수 있습니다.
@Observable class Donut {
var name: String
}
struct DonutView: View {
@Bindable var donut: Donut
var body: some View {
TextField("Name", text: $donut.name)
}
}

@State@Environment@BindableSwiftUI에서는 Observable 모델을 가지고 해당 모델의 인스턴스를 추적하여 변경되면 뷰를 업데이트 한다고 했는데, 이것이 지켜지지 않는 경우도 있습니다.
Observable의 기본 규칙은 사용 중인 프로퍼티가 변경되면, 뷰도 업데이트 된다는 것이다. 이 규칙이 적용되지 않는 경우도 있는데 이럴때는 어떻게 해야될까요?
만약 연산 프로퍼티에 함께 구성된 저장 프로퍼티가 없으면 Observation과 함께 작동하려면 두 가지 추가 단계를 수행해야 합니다. 이는 Observable 타입에 저장된 프로퍼티 구성에 의해 변경되지 않았을때만 필요한 조치입니다.
이 경우에는 프로퍼티에 액세스할 때와 프로퍼티가 변경될 때만 Observation에 알리기만 하면 됩니다.
@Observable class Donut {
var name: String {
get {
access(keyPath: \.name)
return someNonObservableLocation.name
}
set {
withMutation(keyPath: \.name) {
someNonObservableLocation.name = newValue
}
}
}
}
대부분의 경우해당 모델의 속성은 저장된 다른 속성에서 구성되므로 이러한 수작업은 필요하지 않습니다.
하지만 이렇게 Observation을 수동으로 커스텀해서 사용할 수도 있다는 것을 알 수 있습니다.
기존에 사용했던 ObservableObject를 @Observable로 변경했을때 코드들을 어떻게 작성해야 되는지 살펴보겠습니다.
class FoodTruckModel: ObservableObject {
@Published var orders: [Order] = []
@Published var donuts = Donut.all
var orderCount: Int { orders.count }
}
struct AccountView: View {
@ObservedObject var model: FoodTruckModel
@EnvironmentObject private var accountStore: AccountStore
@Environment (\.authorizationController) private var authorizationController
@State private var isSignUpSheetPresented = false
@State private var isSignOutAlertPresented = false
}
@Observable 사용@Observable class FoodTruckModel {
var orders: [Order] = []
var donuts = Donut.all
var orderCount: Int { orders.count }
}
struct AccountView: View {
var model: FoodTruckModel
@Environment(AccountStore.self) private var accountStore
@Environment(AuthorizationController.self) private var authorizationController
@State private var isSignUpSheetPresented = false
@State private var isSignOutAlertPresented = false
}
SwiftUI에서 ObservableObject에서 @Observable로의 전환은 데이터 관리 방식을 더욱 단순화하고 효율화하여 편의성을 높여줍니다. 이를 통해 세 가지 기본 프로퍼티 래퍼(State, Bindable, Environment)로 코드를 작성할 수 있습니다.
이렇게 단순화되어 생각해야 할 설정이 줄어들어서 새로운 기능 추가나 코드 유지 관리가 더욱 용이 해진것 같습니다.
참고
https://developer.apple.com/videos/play/wwdc2023/10149/
https://green1229.tistory.com/373