[수위프트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()
            }
        }
    }
}

위 코드에선 CounterViewModelObservableObject 프로토콜을 따르기 때문에 뷰모델을 @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 프로퍼티 래퍼가 붙어있기 때문에 바뀔 때마다 카운터 화면이 새로 그려질 것입니다.

아래 자료를 보시면 새로운 무작위 숫자가 생성될 때 카운터 숫자가 초기화되는 것을 확인하실 수 있습니다.

`@StateObject`가 아닌 `@ObservedObject`를 썼기 때문에 카운터가 초기화 되는 모습


이를 해결하는 가장 쉬운 방법은 뷰 모델을 감싸고 있는 @ObservedObject 대신에 @StateObject를 사용하는 것입니다. 앞서 설명드렸듯이, @StateObject로 감싼 뷰모델은 화면이 새로 그려져도 카운터 숫자가 그대로 남아있습니다.

그래서 @StateObject는 언제 써야하나요?

여기까지 두 프로퍼티 래퍼가 어떻게 작동하는지 알았으니, @StateObject를 쓰면 더 좋은 경우가 어떤 게 있을지 알아보겠습니다.

SwiftUI가 화면을 만들거나 다시 그릴 수 있는 가능성이 있는 경우엔 내부에 @ObservedObject를 쓰는 것은 안전하지 않습니다. @ObservedObject 객체를 외부에서 주입하는 것이 아니라면 @StateObject를 사용하는 것이 화면이 다시 그려져도 항상 같은 결과를 얻을 수 있을 것입니다.

그렇다면 동일한 객체를 가져다 쓰는 모든 뷰에선 @StateObject를 쓰는게 맞을까요?

동일한 @StateObject 인스턴스를 관찰하고 있는 자식들은 객체를 프로퍼티 래퍼로 표시할 필요가 없는데요. 그렇게 되면 여러군데에서 객체의 라이프사이클을 관리하게 돼서 그러지 않는 것이 좋습니다. 앞 섹션에서 말씀드렸듯이 ObservedObject를 주입하는 경우엔 @ObservedObject를 사용하는 것이 좋습니다.

결론

@StateObject@ObservedObject는 비슷한 특징을 가졌지만 SwiftUI가 그들의 라이프사이클을 관리하는 방식엔 확실한 차이점이 존재합니다. @StateObject를 사용하면 ObservedObject를 생성하는 화면에서도 일관된 결과를 보장할 수 있습니다. 의존 관계로 주입하는거라면 @ObservedObject를 사용할 수 있겠습니다.

감사합니다!