dev/까먹지마

swiftUI @StateObject, @ObservedObject, @EnvironmentObject 정리

Ssul 2025. 4. 18. 12:37

1. StateObject

- view가 인스턴스를 생성하고 소유함

- 보통 ViewModel이 해당 뷰에서만 쓰이거나, 상위뷰에서 초반에 전역으로 사용할 인스턴스 생성할때 사용

 

struct MyView: View {
	@StateObject var viewModel = MyViewModel()
}

//상위에서 생성해서 보낼때 사용
struct TmpApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
    @StateObject var container: DIContainer = .init(services: Services())
    
    var body: some Scene {
        WindowGroup {
            AuthenticatedView(authViewModel: .init(container: container))
                .environmentObject(container)
        }
    }
}

 

 

2. ObservedObject

- view가 인스턴스를 참조만 함

- 상위에서 주입된 ViewModel을 하위로 전달

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

    func increase() {
        count += 1
    }

    func reset() {
        count = 0
    }
}


// 여기서 ViewModel을 생성하고, 전달
struct MainView: View {
    @StateObject var viewModel = CounterViewModel() // 인스턴스 생성

    var body: some View {
        VStack(spacing: 30) {
            Text("MainView에서 count: \(viewModel.count)")

            ChildView1(viewModel: viewModel) // 같은 인스턴스 전달
            ChildView2(viewModel: viewModel)
        }
        .padding()
    }
}

// 전달받은 뷰모델 사용
struct ChildView1: View {
    @ObservedObject var viewModel: CounterViewModel

    var body: some View {
        VStack {
            Text("ChildView1 count: \(viewModel.count)")
            Button("➕ 증가") {
                viewModel.increase()
            }
        }
    }
}

// 전달받은 뷰모델 사용
struct ChildView2: View {
    @ObservedObject var viewModel: CounterViewModel

    var body: some View {
        VStack {
            Text("ChildView2 count: \(viewModel.count)")
            Button("🧹 리셋") {
                viewModel.reset()
            }
        }
    }
}

 

 

3. EnvironmentObject

- 상위에서 전달되면 하위의 어떤 view도 사용가능

- ViewModel에선 사용불가(다른 과정을 거쳐야 함)

//상위에서 생성해서 보낼때 사용
struct TmpApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
    @StateObject var container: DIContainer = .init(services: Services())
    
    var body: some Scene {
        WindowGroup {
            AuthenticatedView(authViewModel: .init(container: container))
                .environmentObject(container)
        }
    }
}

// 이렇게 받으면 됩니다.
struct AuthenticatedView: View {
    @StateObject var authViewModel: AuthenticatedViewModel
    @EnvironmentObject var container: DIContainer
    ...
}

 

4. (중요, 생각보다 많이 필요)서버를 호출하는 EnvironmentObject container를 다른 ViewModel에서 사용하고 싶을때

- 최상위에서 container 주입 > 어떤 하위뷰에서든 container 사용가능, But ViewModel은 사용불가

- 하위 ViewModel에서 container변수를 선언하고, init 구현

- 해당 view에서 EnvironmentObject container를 활용하여 초기화때 ViewModel생성

//최상위에서 주입
struct MyApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
    @StateObject var container: DIContainer = .init(services: Services())
    
    var body: some Scene {
        WindowGroup {
            AuthenticatedView(authViewModel: .init(container: container))
                .environmentObject(container)
        }
    }
}

// ViewModel에서 container사용하려고, 변수 마련
class AnalysisViewModel: ObservableObject { 
    private let container: DIContainer
    
    init(container: DIContainer) {
        self.container = container
    }
    
}

// View에서 EnvironObject container를 사용하여, 초기화
struct AnalysisView: View {
    @EnvironmentObject var container: DIContainer
    @StateObject private var viewModel: AnalysisViewModel
    
    init(container: DIContainer) {
        _viewModel = StateObject(wrappedValue: AnalysisViewModel(container: container))
    }
}

// 그런데 문제, EnvironmetObject임에도 불구하고, AnalysisView호출시 container를 주입해줘야 함
// EnvironmentObject는 View가 초기화 되고 난 후, 자동주입 > 즉 초기화때는 없음 > 그래서 넣어줘야 함
AnalysisView(container: container)

- View는 초기화 된 이후, @EnvironmentObject는 View가 body를 그릴 때 값을 자동 주입.

- 근데 우리는 init할때, container를 사용. 그래서 불편하지만, AnalysisView호출시 container 넣어줘야 함

 

5. 실행순서 이해하기

struct AnalysisView: View {
    @StateObject private var viewModel = AnalysisViewModel()

    var body: some View {
        Text("상태: \(viewModel.status)")
    }
}

1️⃣ 부모 뷰가 AnalysisView() 호출

2️⃣ SwiftUI가 AnalysisView 인스턴스를 생성

3️⃣ @StateObject로 인해 AnalysisViewModel() 실행됨
   → 여기서 ViewModel 인스턴스 최초 생성

4️⃣ ViewModel은 이 View가 **소유**함 (참조 유지)

5️⃣ View의 body 호출

6️⃣ body 안에서 viewModel 사용 가능 (이미 준비됨)

7️⃣ 이후 @Published 값 변경 시 View 자동 업데이트

 

struct AnalysisView: View {
    @EnvironmentObject var container: DIContainer
    @StateObject private var viewModel: AnalysisViewModel

    init(container: DIContainer) {
        _viewModel = StateObject(wrappedValue: AnalysisViewModel(container: container))
    }

    var body: some View {
        Text("분석 중: \(viewModel.status)")
    }
}

1️⃣AnalysisView(container: ...)

2️⃣init(container:) 실행 → ViewModel 생성됨 (@StateObject)

3️⃣SwiftUI가 body 호출 → @EnvironmentObject 자동 주입됨

4️⃣View 그려짐

6️⃣ViewModel 값 바뀜 (@Published) → View 자동 리렌더링

 

 

6. 왜 이렇게 사용하는가?

@StateObject private var viewModel = AnalysisViewModel(container: container)
// 이렇게 사용안하고

init(container: DIContainer) {
        _viewModel = StateObject(wrappedValue: AnalysisViewModel(container: container))
    }  
// 이렇게 사용하는 이유

View의 순서를 제대로 이해해야 한다.

1️⃣속성을 선언만 한다. 이런 값이 들어올꺼야(예: let container: DIContainer)

- 이때 값을 대입하는 것은 불가 > @StateObject private var viewModel = AnalysisViewModel(container: container) 불가!!

2️⃣init() 하는 시점에 다른 외부값을 사용할수 있다

- 외부의존성 사용

- ViewModel 생성

3️⃣View body가 호출됨(화면 그려짐)

- viewModel.status등 사용 가능

4️⃣ViewModel의 상태(@Published) 변경 시, View가 자동으로 다시 렌더링

 

7. 실무에서 추가 팁

DIContainer를 ViewModelFactory처럼 만들기

class DIContainer: ObservableObject {
    let services: Services

    func makeAnalysisViewModel() -> AnalysisViewModel {
        AnalysisViewModel(container: self)
    }
}


@EnvironmentObject var container: DIContainer
@StateObject var viewModel: AnalysisViewModel

init(container: DIContainer) {
    _viewModel = StateObject(wrappedValue: container.makeAnalysisViewModel())
}