[SwiftUI] Data Flow — StateObject vs ObservedObject

J_sung_0o0
8 min readSep 20, 2020

--

ObservableObject 프로토콜을 준수하는 클래스는 StateObject 와 ObservedObject 속성을 사용하여 관리할 수 있습니다. Observable object 를 다룬다는 점에서 유사한 두 속성에 대해서 알아보겠습니다.

@StateObject

StateObject 가 뭔지 간단하게 알아보겠습니다.

StateObject는 아래와 같습니다.

observable object를 인스턴스화 하는 프로퍼티 wrapper 타입

정의를 보면 observable object라는 게 뭐지? 라는 의문이 있을 겁니다. 여기서 observable objectObservableObject 프로토콜을 준수하는 객체를 말합니다. 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 클래스를 생성합니다. TimeCounter0 을 초기값으로 하고 @Published 속성을 갖는 변수 count 를 선언해줍니다.

class TimeCounter: ObservableObject {
@Published var count: Int = 0
func increase() {
count += 1
}
}

위 모델을 CounterView 라는 뷰에서 @ObservedObject 속성을 사용해 counter 라는 객체를 생성합니다. 해당 뷰의 “Tap” 버튼을 누를 때마다 countercount 값이 변경되고 이 값이 @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가 어느 부모 뷰에 속한다고 가정하겠습니다. 부모 뷰의 bodyState 속성의 프로퍼티에 의해 업데이트 되면 CountView 의 화면은 어떻게 될까요?

무조건 숫자가 0으로 초기화 됩니다. 부모뷰의 body가 업데이트 되면서 그 안의 CountViewstruct 의 특징에 따라 다시 생성되게 되고 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.

요약하자면…

  • @StateObjectObservableObject를 인스턴스화를 하고 observable object 내에 @Published 속성으로 선언된 프로퍼티의 값이 변할 때 뷰를 업데이트 합니다. 뷰의 라이플 사이클에 의존하지 않습니다. SwiftUI2 부터 사용가능합니다.
  • @ObservedObject 는 상위 뷰에서 하위 뷰로 인스턴스화 된 observable object를 전달할 때 사용합니다. 뷰의 라이플 사이클에 의존합니다.

참고

--

--

J_sung_0o0
J_sung_0o0

Written by J_sung_0o0

Apple WWDC 19 & 20 Winner (The 1st two-time winner of all time from South Korea)