[수위프트UI/번역] @StateObject와 @ObservedObject, 무엇이 다를까요?
@StateObject
와 @ObservedObject
는 관찰중인 객체의 변경에 반응해서 화면을 업데이트할 수 있게 해주는 SwiftUI의 프로퍼티 래퍼입니다. 둘은 겉보기엔 비슷해 보이지만, SwiftUI 어플리케이션을 만들 때 꼭 알아야하는 뚜렷한 차이를 가지고 있습니다.
This article was originally written by Antoine van der Lee.
이 글은 Antoine van der Lee가 작성한 글의 번역본입니다. 원문 링크
글쓴이는 처음엔 항상 @ObservedObject
를 쓰면 되는게 아닌가 생각할 정도로 닮아서 혼란스러웠지만, SwiftUI에서 @StateObject
의 용도를 알고난 이후엔 @StateObject
가 얼마나 중요한 존재인지 알게되었다고 합니다.
@ObservedObject
는 무엇일까요?
@StateObject
와 @ObservedObject
의 차이에 대해 깊이 알아보기 전에, @ObservedObject
가 무엇인지 이해하는 것이 좋겠습니다. 두 프로퍼티 래퍼 모두 ObservableObject
프로토콜을 따르는 객체를 필요로 합니다. 이 프로토콜은 객체의 값이 바뀌기 전에 알려주는 퍼블리셔를 의미하며, SwiftUI가 화면을 다시 그리는 것을 가능하게 합니다.
그러니 ObservableObject
를 따른다면 @ObservedObject
프로퍼티 래퍼와 함께 SwiftUI 화면에 연결되어 데이터가 변경되었을 때 화면을 다시 그릴 수 있게 됩니다.
대표적인 예시는 카운터가 있겠습니다.
final class CounterViewModel: ObservableObject {
@Published var count = 0
func incrementCounter() {
count += 1
}
}
struct CounterView: View {
@ObservedObject var viewModel = CounterViewModel()
var body: some View {
VStack {
Text("Count is: \(viewModel.count)")
Button("Increment Counter") {
viewModel.incrementCounter()
}
}
}
}
위 코드에선 CounterViewModel
이 ObservableObject
프로토콜을 따르기 때문에 뷰모델을 @ObservedObject
로 정의할 수 있었습니다. 뷰모델 내부의 count
값은 버튼을 누를 때마다 증가하게 되고, @Published
가 변화했다는 신호를 쏴서 다른 부분에서 변화를 알게 합니다.
값이 변화하는 것을 좀 더 직관적으로 이해하기 위해 @Published
프로퍼티 래퍼 대신 값을 바꾼 후 신호를 직접 쏴보겠습니다.
final class CounterViewModel: ObservableObject {
private(set) var count = 0
func incrementCounter() {
count += 1
objectWillChange.send()
}
}
위 코드를 실행해보셨다면 여전히 값이 변경되면 화면이 업데이트되는 것을 확인하셨을 것입니다. 실제로 objectWillChange.send()
메소드를 수동으로 쏘는 경우는 거의 없지만, 여러 값을 변경 후 수동으로 신호를 쏘는 것이 어떨 때는 더 나은 해결책이 될 수도 있습니다.
위의 예시 코드는 SwiftUI가 @ObservedObject
내부에서 일어나는 변화를 기반으로 어떻게 화면을 다시 그리는지 보여줍니다. 다음으로는 @StateObject
에 대해서 알아봅시다.
@StateObject
는 무엇인가요?
@StateObject
프로퍼티 래퍼는 겉보기엔 @ObservedObject
와 비슷하게 작동합니다. 앞에 나온 예시의 코드의 @ObservedObject
를 @StateObject
로 바꿨지만 큰 차이가 없어보입니다.
struct CounterView: View {
/// `@ObservedObject` 대신 `@StateObject`를 사용한 모습
@StateObject var viewModel = CounterViewModel()
var body: some View {
VStack {
Text("Count is: \(viewModel.count)")
Button("Increment Counter") {
viewModel.incrementCounter()
}
}
}
}
여기까지 봤을 땐 글쓴이가 @ObservedObject
를 주로 사용하려고 했던 것에 공감이 되실겁니다. 하지만 @ObservedObject
대신 @StateObject
를 사용하는 것에는 명확한 차이점이 존재합니다.
@StateObject
를 통해서 관찰되고 있는 객체는 그들을 가지고 있는 화면 구조가 재생성되어도 파괴되지 않습니다. 이 차이점은 화면이 다른 화면을 포함하고 있는 경우를 생각하면 이해하기 쉽습니다.
@StateObject
가 작동하는 방식을 알기 위해 기존의 카운터 코드를 다른 코드 안에 넣어보겠습니다
struct RandomNumberView: View {
@State var randomNumber = 0
var body: some View {
VStack {
Text("Random number is: \(randomNumber)")
Button("Randomize number") {
randomNumber = (0..<1000).randomElement()!
}
}.padding(.bottom)
CounterView()
}
}
무작위 숫자 화면은 Randomize 버튼을 통해 새로운 무작위 숫자를 생성합니다. randomNumber
에는 @State
프로퍼티 래퍼가 붙어있기 때문에 바뀔 때마다 카운터 화면이 새로 그려질 것입니다.
아래 자료를 보시면 새로운 무작위 숫자가 생성될 때 카운터 숫자가 초기화되는 것을 확인하실 수 있습니다.
이를 해결하는 가장 쉬운 방법은 뷰 모델을 감싸고 있는 @ObservedObject
대신에 @StateObject
를 사용하는 것입니다. 앞서 설명드렸듯이, @StateObject
로 감싼 뷰모델은 화면이 새로 그려져도 카운터 숫자가 그대로 남아있습니다.
그래서 @StateObject
는 언제 써야하나요?
여기까지 두 프로퍼티 래퍼가 어떻게 작동하는지 알았으니, @StateObject
를 쓰면 더 좋은 경우가 어떤 게 있을지 알아보겠습니다.
SwiftUI가 화면을 만들거나 다시 그릴 수 있는 가능성이 있는 경우엔 내부에 @ObservedObject
를 쓰는 것은 안전하지 않습니다. @ObservedObject
객체를 외부에서 주입하는 것이 아니라면 @StateObject
를 사용하는 것이 화면이 다시 그려져도 항상 같은 결과를 얻을 수 있을 것입니다.
그렇다면 동일한 객체를 가져다 쓰는 모든 뷰에선 @StateObject
를 쓰는게 맞을까요?
동일한 @StateObject
인스턴스를 관찰하고 있는 자식들은 객체를 프로퍼티 래퍼로 표시할 필요가 없는데요. 그렇게 되면 여러군데에서 객체의 라이프사이클을 관리하게 돼서 그러지 않는 것이 좋습니다. 앞 섹션에서 말씀드렸듯이 ObservedObject
를 주입하는 경우엔 @ObservedObject
를 사용하는 것이 좋습니다.
결론
@StateObject
와 @ObservedObject
는 비슷한 특징을 가졌지만 SwiftUI가 그들의 라이프사이클을 관리하는 방식엔 확실한 차이점이 존재합니다. @StateObject
를 사용하면 ObservedObject
를 생성하는 화면에서도 일관된 결과를 보장할 수 있습니다. 의존 관계로 주입하는거라면 @ObservedObject
를 사용할 수 있겠습니다.
감사합니다!