iOS

[SwiftUI] @ObservedObject vs @StateObject

띵지니어 2025. 2. 10. 18:57
반응형

 

 

안녕하세요 띵지니어 입니다. 😼

오늘은 SwiftUI 에서 상태 관리에 많이 사용되는 @ObservedObject와 @StateObject에 대해 포스팅하겠습니다.

정의

두 개의 정의는 다음과 같습니다.

https://developer.apple.com/documentation/swiftui/observedobject

- @ObservedObject: 관찰 가능한 객체를 구독하고 관찰 가능한 객체가 변경될 때마다 뷰를 무효화(렌더링) 하는 속성 래퍼 유형입니다.

- @StateObject: 관찰 가능한 객체를 인스턴스화하는 프로퍼티 래퍼 유형입니다.

두 개의 공통점은 Observable 객체가 변경되면 뷰를 업데이트 시켜주는 기능이 있는 property wrapper입니다.

여기서 Observable객체는 뭘까요? (observed 랑 다른 개념 입니다!!)

 

ObservableObject | Apple Developer Documentation

A type of object with a publisher that emits before the object has changed.

developer.apple.com

ObservableObject: 객체가 변경되기 전에 방출되는 @Published 변수가 존재하는 객체(Object) 유형입니다.

쉽게 말해서, Observable 한 모델의 값이 변경이 되면 "변경된 값을 뷰에 반영할 수 있구나"라고 생각하면 됩니다.

 

사용 방법

1. ObservableObject를 채택하는 클래스(viewModel)를 만들고, @Published 프로퍼티 래퍼 속성으로 변수를 선언합니다.
struct는 안되고 class 만 됩니다. (실제로 컴파일 에러)

제 생각으로 struct가 안 되는 이유를 생각해 보면 ?
변경가능한 객체를 값(struct) 타입으로 하게 되면 값이 변경되기 때문에 새로운 인스턴스가 복사되어야 하고,
ObservableObject 계산 속성인 objectWillChange로 데이터를 방출할 때 메모리 효율이 안 좋아서 일 것이라고 생각합니다.

class ViewModel: ObservableObject { // ObservableObject 를 채택 하는 클래스를 만들고
    @Published var isOn = false // @Published 프로퍼티 래퍼 속성으로 변수를 선언
    
    func toggle() {
        isOn.toggle()
    }
}

 

2. 뷰가 @ObservedObject property wrapper를 사용하여 ObservableObject를 구독(subscribe)합니다.

이렇게 된다면, @Published 선언된 변수가 변경될 때, 변경사항을 ViewModel을 사용하는 곳에 방출을 해줍니다.
MyParentView에서 ViewModel을 가지고 있다면, 내부 isOn가 변경되었을 때, isOn를 사용하고 있는 뷰계층 구조를 다시 그리게 됩니다.

ObservedObject를 StateObject로 바뀌어도 동작 방식은 똑같이 적용됩니다.

struct MyParentView: View {
    @ObservedObject var viewModel = ViewModel() // @StateObject 로 바꿔도 같은 동작
    
    var body: some View {
        VStack {
            Button(viewModel.isOn ? "On" : "Off") {
                viewModel.isOn.toggle()
            }
        }
    }
}

 


 

@ObservedObject vs @StateObject 

그럼 두 개의 차이는 뭐고, 언제 사용하면 될까요??

 

먼저 @ObservedObject부터 알아보겠습니다.

@ObservedObject는 부모 뷰에서 상태값이 업데이트되어 뷰가 다시 그려질 때,
ObservableObject를 구독하고 있는 subview의 상태값이 초기화되는 property wrapper입니다.

실제 코드를 보면 아래처럼 사용할 수 있습니다.

ObservableObject를 채택하는 ViewModel을 만들어 줍니다.

class ViewModel: ObservableObject {
    @Published var isOn = false
    
    func toggle() {
        isOn.toggle()
    }
}

 

다음은 isOn이라는 상태를 가지고 있는 MyParentView을 만들었습니다.

struct MyParentView: View {
    @State var isOn = false
    
    var body: some View {
        VStack {
            Button(isOn ? "상위뷰 변경 값" : "상위뷰 초기 값") {
                isOn.toggle()
            }
            Divider()
            
            MySubView()
        }
    }
}

 

마지막으로 MySubView라는 자식 뷰를 만들어 줍니다.
이 뷰는 @ObservedObject 한 viewModel을 내부에서 생성해주었습니다.

struct MySubView: View {
    @ObservedObject var viewModel = ViewModel()
    
    var body: some View {
        Button(viewModel.isOn ? "하위뷰 변경 값" : "하위뷰 초기 값") {
            viewModel.toggle()
        }
    }
}

이제 MyParentView를 Preview를 사용해서 ObservedObject 가 어떻게 바뀌는지 봅니다.

하위뷰의 속성을 변경했는데, 상위뷰를 변경하니까 하위뷰의 속성이 초기화가 되는 것을 볼 수 있습니다.

 

다음은 @StateObject

struct MySubView: View {
    @StateObject var viewModel = ViewModel() // @ObservedObject 에서 @StateObject 로 변경
    
    var body: some View {
        Button(viewModel.isOn ? "하위뷰 변경 값" : "하위뷰 초기 값") {
            viewModel.toggle()
        }
    }
}

 

 @ObservedObject를 @StateObject로 변경해주었습니다.

이번에도 상위 뷰의 값이 변경될 때 하위 뷰가 어떻게 되는지 봅시다.

이 경우 상위 뷰의 값을 변경해 주어도, 하위 뷰를 초기화하지 않습니다.

 

이유는 @StateObject는 뷰가 처음 생성될 때 한 번만 인스턴스를 초기화하기 때문인데요

@StateObject의 메모리 주소가 유지가 되기 때문에 내부적으로 뷰가 재렌더링되더라도 같은 객체를 참조하도록 합니다!
따라서 같은 뷰에서 상태가 변경되고, 뷰가 다시 렌더링 될 때도 @StateObject가 유지됩니다.


마무리

두 개의 특성에 따라서, 저는 아래처럼 사용하고 있습니다.

1. 특정 뷰가 직접 생성한 상태를 유지해야 할 때는 @StateObject를 쓰는 것을 권장합니다.

struct CounterView: View {
    @StateObject private var viewModel = CounterViewModel() // 뷰에서 직접 생성

    var body: some View {
        VStack {
            Text("Count: \(viewModel.count)")
            Button("Increment") {
                viewModel.count += 1
            }
        }
    }
}

 

2. 외부(부모)에서 주입을 받는 Observable 객체 일 때, @ObservedObject를 사용합니다.

class CounterViewModel: ObservableObject {
    @Published var count = 0
}

struct ParentView: View {
    @StateObject private var viewModel = CounterViewModel() // 부모에서 생성

    var body: some View {
        ChildView(viewModel: viewModel) // 자식에게 주입
    }
}

struct ChildView: View {
    @ObservedObject var viewModel: CounterViewModel // 외부(부모)에서 주입받음

    var body: some View {
        VStack {
            Text("Count: \(viewModel.count)")
            Button("Increment") {
                viewModel.count += 1
            }
        }
    }
}

정리

조건 @ObservedObject  @StateObject
부모가 만든 ViewModel을 자식에서 사용할 때 ✅ 사용 🚫 사용하지 않음
뷰가 직접 ViewModel을 생성하고 관리할 때 🚫 사용하지 않음 ✅ 사용
뷰가 다시 렌더링될 때 객체를 유지해야 할 때 🚫 초기화될 가능성 있음 ✅ 유지됨

 

- 참고

https://developer.apple.com/documentation/swiftui/stateobject

 

StateObject | Apple Developer Documentation

A property wrapper type that instantiates an observable object.

developer.apple.com

https://developer.apple.com/documentation/Combine/ObservableObject

 

ObservableObject | Apple Developer Documentation

A type of object with a publisher that emits before the object has changed.

developer.apple.com

https://ios-development.tistory.com/1160

 

[iOS - SwiftUI] @ObservedObject, @StateObject 개념, 차이점, 사용 방법 (MVVM 패턴)

목차) SwiftUI의 기본 - 목차 링크 * @Published, @objecervableObject 개념은 Combine이므로, Combine 관련 이전 포스팅 글 참고 @ObservedObejct 란? observable 객체를 구독하는 property wrapper observable 객체가 변경되면 뷰

ios-development.tistory.com

반응형
목차(index)