[SwiftUI] Data Flow — StateObject vs ObservedObject
ObservableObject 프로토콜을 준수하는 클래스는 StateObject 와 ObservedObject 속성을 사용하여 관리할 수 있습니다. Observable object 를 다룬다는 점에서 유사한 두 속성에 대해서 알아보겠습니다.
@StateObject
StateObject 가 뭔지 간단하게 알아보겠습니다.
StateObject는 아래와 같습니다.
observable object를 인스턴스화 하는 프로퍼티 wrapper 타입
정의를 보면 observable object라는 게 뭐지? 라는 의문이 있을 겁니다. 여기서 observable object 란 ObservableObject 프로토콜을 준수하는 객체를 말합니다. ObservableObject의 정의는 아래와 같습니다.
객체가 변하기 전에 퍼블리싱하는 퍼블리셔를 갖는 오브젝트 타입
ObservableObject
를 준수하는 클래스는 @Published 속성으로 선언된 변수가 값이 변하려고 할 때(willSet
) 외부에 퍼블리싱을 합니다.
class UserModel: ObservableObject {
@Published var userID: String = ""}
위의 예제 코드에 따라 UserModel
객체를 바라보고 있는 다른 객체는 @Published
속성으로 선언된 userID
값이 변하려고 할 때 값의 변화를 전달 받을 수 있습니다.
struct SignInView: View {
@StateObject var user = UserModel() var body: some View {
VStack {
if !uesr.userID.isEmpty { Text("Hi, \(user.userID)) } SignInButton()
.onTapGesture { user.userID = "Jaesung" }
}
}
}
위의 예시 코드는 로그인 하는 간단한 뷰 입니다.
@StateObject
를 통해 ObservableObject
를 준수하는 UserModel
를 인스턴스화 하고 있습니다.
버튼을 탭하면 UserModel 의 userID
값이 “Jaesung” 으로 변하게 됩니다. userID는 @Published
속성으로 선언되어 있기 때문에 값이 바뀌기 전에 값이 바뀔 거라고 퍼블리싱을 할 것입니다.
값이 바뀌었다는 이벤트를 받은 @StateObject
에 의해 뷰는 다시 body
를 계산하여 화면에 “Welcome, Jaesung” 이라는 텍스트가 띄어지게 됩니다.
@ObservedObject
이번에는 ObservedObject 에 대해 간단히 알아보겠습니다.
이전에 SwiftUI가 처음 등장했을 때는 지금의 StateObject
의 역할까지 ObservedObject
가 수행하였습니다. (그 당시엔 정의도 없었습니다). 하지만 SwiftUI 2 가 공개되고 StateObject
가 등장함에 따라 ObservedObject
의 역할이 좀 더 명확해졌습니다.
애플에 따르면 ObservedObject의 정의는 아래와 같습니다.
Observable object를 서브스크라입 하고 해당 observable object에 변화가 있을 때마다 뷰를 무효화를 시킨다.
앞서 알아본 StateObject
와 유사해 보입니다. 하지만 다른 점이 있다면 인스턴스화에 대한 내용이 없습니다. 왜냐면 ObservedObject
는 이미 생성된 객체를 바라보기 때문입니다.
때문에 ObservedObject 는 하나의 뷰에서 다른 뷰로 Observable Object 객체를 전달할 때 사용됩니다. 즉 @StateObject
속성으로 이미 인스턴스화 된 observable object 를 뷰 계층에서 아래에 속한 뷰들(Child view)에게 @ObservedObject
속성을 통해 전달할 수 있습니다.
struct SignInView: View {
@StateObject var user = UserModel() var body: some View {
NavigationLink("ShowProfile", destination: ProfileView(user: user)
}
}
SignInView
의 하위 뷰인 ProfileView
는 @ObservedObject
속성을 통해 UserModel
인스턴스를 넘겨 받습니다.
struct ProfileView: View {
@ObservedObject var user: User var body: some View {
Text("User ID: \(user.userID)")
}
}
그래서 둘의 차이가 뭐죠…?
아래 처럼 ObservableObject
프로토콜을 준수하는 TimeCounter
클래스를 생성합니다. TimeCounter
는 0
을 초기값으로 하고 @Published
속성을 갖는 변수 count
를 선언해줍니다.
class TimeCounter: ObservableObject {
@Published var count: Int = 0 func increase() {
count += 1
}
}
위 모델을 CounterView
라는 뷰에서 @ObservedObject
속성을 사용해 counter
라는 객체를 생성합니다. 해당 뷰의 “Tap” 버튼을 누를 때마다 counter
의 count
값이 변경되고 이 값이 @Published
속성이므로 ObservableObject
프로토콜을 통해 업데이트 된 값을 바탕으로 뷰를 다시 그리게 됩니다. 즉, 화면에 0
으로 떠있던 숫자가 “Tap” 을 누를때마다, 1
, 2
, 3
이렇게 값이 증가하여 보여지게 됩니다.
struct CountView: View {
@ObservedObject var counter = TimeCounter() var body: some View {
VStack {
Text(counter.count)
Button(action: counter.increase) {
Text("Tap")
}
}
}
}
이때 CountView
가 어느 부모 뷰에 속한다고 가정하겠습니다. 부모 뷰의 body
가 State 속성의 프로퍼티에 의해 업데이트 되면 CountView
의 화면은 어떻게 될까요?
무조건 숫자가 0
으로 초기화 됩니다. 부모뷰의 body
가 업데이트 되면서 그 안의 CountView
도 struct
의 특징에 따라 다시 생성되게 되고 CountView
안의 observed object
도 초기화가 됩니다. 즉, 모델이 뷰의 라이프 사이클에 의존을 하고 있습니다.
그렇다면 @StateObject
는 어떨까요?
위의 CountView
코드에서 아래와 같이 @ObservedObject
속성을 @StateObject
속성으로 변경해보겠습니다.
struct CountView: View {
@StateObject var counter = TimeCounter()
...
}
이 상황에서 부모뷰가 업데이트가 되면 body
가 업데이트 되면서 분명 CountView
를 재생성하게 될 것입니다. 하지만 CountView
에서 보여지는 숫자는 절대로 초기화 되지 않습니다. 만약 CountView
의 숫자가 2
였다면 부모뷰가 업데이트 되어도 여전히 2
를 보여주게 됩니다.
@StateObject
는 @ObservedObject
와 달리 모델을 “참조(source of truth)” 하기 때문에 뷰의 라이프 사이클에 의존하지 않습니다. 뷰가 사라져도 StateObject
프로퍼티는 여전히 살아있습니다.
StateObject: when you need to create a reference type inside one of your views and make sure it stays alive for use in that view and others you share it with.
ObservedObject: You can get same result as StateObject, but that’s dangerous — sometimes, and only sometimes, it can accidentally release the object it was storing, because it wasn’t designed to be the ultimate source of truth for the object.
요약하자면…
@StateObject
는ObservableObject
를 인스턴스화를 하고 observable object 내에@Published
속성으로 선언된 프로퍼티의 값이 변할 때 뷰를 업데이트 합니다. 뷰의 라이플 사이클에 의존하지 않습니다. SwiftUI2 부터 사용가능합니다.@ObservedObject
는 상위 뷰에서 하위 뷰로 인스턴스화 된 observable object를 전달할 때 사용합니다. 뷰의 라이플 사이클에 의존합니다.