View의 부분일 경우 @State, 글로벌하게 변수를 사용할 경우 @Environment(Type.self) var, 단순히 바인딩이 필요할 경우(TextField 같이 값의 변화를 주기 위함) @Bindable 그게 아니라면 var 를 사용하면 된다.
3. 잘 사용하고 있는 ObservableObject 에서 Observable 을 사용해야 하는 이유
- 간단하게 정의한 데이터 타입자체에 Observable 매크로를 사용하여 ViewModel 처럼 활용이 가능하다.
- View 를 Re-render 하는 기준이 다르기 때문에 불필요한 Re-render를 최소화 할 수 있다.
기존 ObservedObject, StateObject, EnvironmentObject 의 경우 내부 프로퍼티 의 변화가 발생하면 해당 객체를 갖고 있는 모든 View는 변화를 감지하여 Re-render 가 이루어진다. 이게 State 와의 가장 큰 차이점이었다. @State 의 경우 변수를 mutate 시키더라도 이를 view 에서 직접적으로 사용하지 않는다면 view 를 re-render 를 시키지 않는다. @Observable 의 경우 사용할때 @State, @Environment 등을 사용하는 것 처럼 내부 프로퍼티의 변화가 있더라도 해당 객체의 변화하는 프로퍼티를 직접 view 에서 사용하지 않는다면 Re-render 를 발생시키지 않는다. 즉 불필요하 Re-render 를 최소화 할 수 있다.
import SwiftUI
final class OriginalObservable: ObservableObject {
@Published var count: Int = 0
func add() {
self.count += 1
}
func minus() {
self.count -= 1
}
}
struct OriginalSubView1: View {
@EnvironmentObject var vm: OriginalObservable
var body: some View {
VStack {
let _ = Self._printChanges()
Text("\(vm.count)")
HStack {
Button(action: { vm.minus() }) {
Text("-")
}
Button(action: { vm.add() }) {
Text("+")
}
}
}
}
}
struct OriginalSubView2: View {
@EnvironmentObject var vm: OriginalObservable
var body: some View {
VStack {
let _ = Self._printChanges()
Text("Original Subview2")
}
}
}
struct ContentView: View {
// 07:29
var body: some View {
VStack {
OriginalSubView1()
OriginalSubView2()
}
}
}
위에 코드를 보게 되면 OriginalSubView2 의 경우 OriginalSubView1 과 같은 EnvironmentObject 를 가지고 있다. 하지만 View에서 OriginalObservable 타입의 어떠한 프로퍼티도 View에서 사용하고 있지 않다.(극적인 예시를 위해.....불필요한 view model 을.....)
동작을 살펴보면
같은 EnvironmentObject를 가지고 있는 모든 View 는 ViewModel 의 변화를 감지하고 View를 Re-render를 시킨다.
그럼 새롭게 도입된 Observable을 확인해보자.
import SwiftUI
import Observation
@Observable final class NewObservable {
var count: Int = 0
func add() {
self.count += 1
}
func minus() {
self.count -= 1
}
}
struct NewSubview1: View {
@Environment(NewObservable.self) var vm: NewObservable
var body: some View {
VStack {
let _ = Self._printChanges()
Text("\(vm.count)")
HStack {
Button(action: { vm.minus() }) {
Text("-")
}
Button(action: { vm.add() }) {
Text("+")
}
}
}
}
}
struct NewSubview2: View {
@Environment(NewObservable.self) var vm: NewObservable
var body: some View {
VStack {
let _ = Self._printChanges()
Text("New Subview2")
}
}
}
struct ContentView: View {
var body: some View {
VStack {
NewSubview1()
NewSubview2()
}
}
}
아까와 동일한 구조를 보이지만 New Subview2 의 경우 Re-render가 발생하지 않는 것을 확인할 수 있다.
- 추적할 프로퍼티에 @Published property wrapper 를 붙여줄 필요가 없다.(코드가 짧아진다)
등의 이유가 있다.
4. 느낌점
여기서 가장 흥미로운 점은 2번째이다. 몇일전 View Re-render 원리에 대한 공부를 했을 때 ViewModel 로 채택한 ObservableObject 는 하나의 프로퍼티의 변경으로 채택한 모든 View 의 변화를 만들어 냈고, 그렇기에 ViewModel 의 설계에 의한 데이터 흐름을 잘 설계해야한다는 것을 느꼈는데, iOS17 부터는 Observable 매크로를 통해서 이 부분을 조금 더 쉽게 효율적인 앱의 동작을 이끌어 낼 수 있다는 점이었다. 요즘 iOS 15, 16 을 최소버전으로 쓰고 있는 앱들이 많은 만큼 곧 iOS17 이 최소버전인 앱들도 생겨날 것이기에, 그 준비를 위해 Observable 을 사용하는 것을 고려해보는 것도 좋을 것 같다.