@ObservedObject, @StateObject 언제, 어떻게, 무엇을 사용해야 하는가?

이정훈·2023년 12월 17일
3

SwiftUI

목록 보기
4/4
post-thumbnail

SwiftUI를 이용해서 MVVM 패턴으로 개발을 하다보면 View에서 관찰 가능한 ViewModel 인스턴스를 만들때 @StateObject, @ObservedObject property wrapper를 사용하게 된다.

프로젝트를 진행 하면서도 항상 둘 중에 무엇을 사용해야 되는가 고민을 하게 되는데 이번 기회에 @StateObject@ObservedObject의 공통점과 차이점, 그리고 어떤 상황에서 무엇을 사용해야 되는지 깔끔하게 정리해 보자


@ObservedObject

시작에 앞서 @ObservedObject property wrapper가 무엇인지 알아보자

View에서 @ObservedObject로 선언된 인스턴스는 해당 인스턴스가 가지고 있는 @Published 변수가 값이 변경 되었을때 이 변화를 감지하고 View를 새롭게 재렌더링할 수 있게 한다.

@ObservedObject로 선언할 수 있는 객체는 ObservableObject protocol을 따르는 클래스 타입의 인스턴스만을 선언할 수 있다.

아래 예시를 통해 다시 한번 알아보자

final class CounterViewModel: ObservableObject {
    @Published var count: Int = 0
    
    func addCount() {
        count += 1
    }
}

CounterViewModel class는 ObservableObject protocol를 채택하고 있으며, count 변수는 @Published property wrapper로 선언되어 있어 count 변수의 값이 변경된다면 SwiftUI에게 데이터가 변경 되었음을 알리고 CounterViewModel class의 인스턴스와 연관 되어 있는 View를 새롭게 렌더링할 수 있다.

정확히 말하자면 값이 변경되면 ObservableObject protocol에서 제공하는 objectWillChange publishersend()라는 메서드를 자동으로 호출하여 데이터가 변경되었음을 알릴 수 있다.

import SwiftUI

struct CounterView: View {
    @ObservedObject var viewModel: ViewModel = ViewModel()
    
    var body: some View {
        VStack(spacing: 20) {
            Text("viewModel.count : \(counterViewModel.count)")
            
            Button(action: {
                counterViewModel.addCount()
            }, label: {
                Text("Add viewModel count")
            })
        }
    }
}

View에 해당하는 CounterView에서는 counterViewModel 변수를 @ObservedObject property wrapper로 선언 하였으며, Button을 터치하게 되면 counterViewModeladdCount 메서드를 호출하여 counterViewModelcount 변수를 1씩 증가 시키고 연관되어 있는 View를 새롭게 렌더링하게 될것이다.


@StateObject

그렇다면 @StateObject property wrapper는 무슨 역할을 할까??

목적 자체는 @ObservedObject와 마찬가지로 해당 인스턴스가 가지고 있는 @Published 변수가 값이 변경되었을 때 이 변화를 감지하고 View를 새롭게 렌더링하는 것으로 동일하다.

따라서 위의 코드에서 @ObservedObject@StateObject로 변경해도 결과는 동일하게 동작한다.

import SwiftUI

struct CounterView: View {
    @StateObject var counterViewModel: CounterViewModel = CounterViewModel()
    
    var body: some View {
        VStack(spacing: 20) {
            Text("viewModel.count : \(counterViewModel.count)")
            
            Button(action: {
                counterViewModel.addCount()
            }, label: {
                Text("Add viewModel count")
            })
        }
    }
}

이제부터 혼란이 시작된다..

그럼 @ObservedObject@StateObject 중에 무엇을 사용해야 되지..?? 그냥 @ObservedObject 쓰면 되는거 아닌가?

property wrapper에는 명확한 차이가 존재하며, 이 차이를 체감하기 위해서 CounterView를 상위 View가 포함하고 있는 형태에서 차이를 확인할 수 있었다.

가령 다음과 같이 CounterView를 포함하고 있는 ContentView에서 두 property wrapper의 차이를 확인 해보자

import SwiftUI

struct ContentView: View {
    @State private var count: Int = 0
    
    var body: some View {
        VStack(spacing: 20) {            
            Text("count: \(count)")
            
            Button(action: {
                count += 1
            }, label: {
                Text("Add")
            })
            
            CounterView()
        }
    }
}

ContentView@State property인 count 변수를 가지고 있으며 Button을 터치하면 count 의 값이 1씩 증가하고 데이터가 변경됨에 따라 View를 새롭게 렌더링 한다.

다음으로 CounterView의 경우 CounterViewModel 클래스 인스턴스를 @ObservedObject property wrapper로 가지고 있다.

import SwiftUI

struct CounterView: View {
    @ObservedObject var counterViewModel: CounterViewModel = CounterViewModel()
    
    //나머지는 위의 코드와 동일
    
}

이때 ContentView의 결과를 확인해보면 다음과 같다.

결과를 확인해보면서 좀 당황스러운 부분은 CotentView에 속한 @State propertycount의 값을 변경하였을때 CounterView에 속한 counterViewModelcounter초기화 되버린다는 것이다.

이 현상의 윈인은 다음과 같다.

CotentView에 속한 count의 값을 변경하였을때 View를 다시 랜더링하면 CounterView도 새로 그려지면서 값이 초기화 되는것..

이번에는 CounterView에 속한 counterViewModel@StateObject로 변경하고 결과를 다시 확인해보자

import SwiftUI

struct CounterView: View {
    @StateObject var counterViewModel: CounterViewModel = CounterViewModel()
    
    //나머지 코드는 위와 동일
    
}

이번에는 CotentView에 속한 count의 값을 변경되어도 CounterView에 속한 CounterViewModelcounter property의 값이 초기화되지 않는다는 점이다.

위의 예시를 통해 우리는

@StateObject로 선언 되어 있으면 View가 재렌더링 되더라도 초기화 되지 않는구나

를 확인해 볼 수 있었다.

여기서 확인해 봐야할 부분은 과연 @StateObject를 포함하는 하위 View재렌더링 되지 않는것을까? 아니면 재렌더링 되더라도 값이 유지 되는 것일까?

결론부터 말하면 재렌더링 되더라도 값이 유지 되는 쪽에 더 가깝다고 할 수 있으며 이것을 확인하기 위해 CounterView 내부에 initializer를 만들어 View가 재렌더링 되는지 확인해 보았다.

import SwiftUI

struct CounterView: View {
    @StateObject var counterViewModel: CounterViewModel = CounterViewModel()
    
    init() {
        print("CounterView initialized")
    }
    
    var body: some View {
        VStack(spacing: 20) {
            Text("viewModel.count : \(counterViewModel.count)")
            
            Button(action: {
                counterViewModel.addCount()
            }, label: {
                Text("Add viewModel count")
            })
        }
    }
}

그리고 결과 확인을 위하여 위와 같이 ContentView에서 버튼을 터치하여 결과를 확인 해보았을때 아래와 같이 하위 ViewCounterView가 다시 생성되는 것을 확인할 수 있었다.

결국 @StateObject property 변수를 가지고 있더라도 상위 View에서 값이 변경되면 마찬가지로 초기화 되지만 그 값은 유지 된다는 점이 @ObeservedObject@StateObject의 가장 큰 차이점이라고 할 수 있겠다.


마무리하며 결론

지금까지 @ObeservedObject@StateObjectproperty wrapper의 차이점이 무엇인지 살펴 보았다.

마지막으로 어떤 상황에서 어떤 property wrapper를 사용하면 좋을지에 대해 이야기 해보고 마무리하려고 한다.

필자는 두 property wrapper의 공통점과 차이점을 토대로 다음과 같이 정리하고 싶다.

상위 View의 값의 변화로 하위 View의 내용이 모두 새롭게 초기화 되어야 한다 -> @ObservedObject 사용

상위 View의 값의 변화에 따라 하위 View에는 영향이 없이 그래도 유지되거나 새로운 값만 View에 추가적으로 렌더링 되어야 한다. -> @StateObject 사용


Reference

https://pilgwon.github.io/post/state-object-vs-observed-object

profile
새롭게 알게된 것을 기록하는 공간

0개의 댓글