반응형

SwiftUI Study – @StateObject와 @ObservedObject를 정확하게 선택하는 법 (뷰 재생성·초기화 문제 방지)

 

1. 왜 중요한가 (문제 배경)

SwiftUI 개발에서 가장 흔하면서도 치명적인 실수는 다음입니다.

  • ViewModel이 계속 재생성된다
  • API가 여러 번 호출된다
  • @ObservedObject를 써서 ViewModel이 사라지는 문제
  • @StateObject를 잘못 사용해 초기화 타이밍이 꼬이는 문제

즉, SwiftUI의 lifecycle을 정확히 이해하지 못하면

의도하지 않은 상태 초기화 / 중복 호출 / 메모리 낭비 / UI 갱신 누락이 발생합니다.

핵심 원칙은 다음과 같습니다.

“해당 ViewModel을 이 View의 생명주기 동안 유지해야 한다면 @StateObject”

“부모에서 만들어진 ViewModel을 전달받아 사용만 한다면 @ObservedObject” 

사용 목적이 서로 완전히 다르다.

 

2. 잘못된 패턴 예시

❌ 예시 1: 매번 View가 그려질 때마다 ViewModel 재생성

struct WrongProfileView: View {
    @ObservedObject var vm = ProfileViewModel()   // ❌ View가 redraw될 때마다 새로 생성됨

    var body: some View {
        Text(vm.name)
    }
}

문제점

- 화면 이동·상태 변화 때마다 ViewModel이 다시 만들어짐

- API가 여러 번 호출됨

- @ObservedObject는 “외부에서 주입된 것”을 의미하므로 내부 생성은 잘못된 패턴

❌ 예시 2: 부모에서 @StateObject를 쓰지 않아 자식 ViewModel도 계속 초기화

struct ParentView: View {
    var body: some View {
        ChildView(vm: ChildViewModel())   // ❌ 매번 새로운 ViewModel 생성
    }
}

문제점

- ChildViewModel이 계속 재생성됨

- 내부 상태가 유지되지 않음

- 특히 NavigationStack 안에서 흔히 보이는 문제

❌ 예시 3: ObservableObject를 여러 View 계층에서 중복 소유

struct WrongListItemView: View {
    @ObservedObject var vm: ItemViewModel

    var body: some View {
        VStack {
            Text(vm.title)
        }
        .onAppear {
            vm.load()   // ❌ 셀이 재사용될 때마다 여러 번 호출
        }
    }
}

문제점

- 리스트 스크롤 시 load()가 무한히 호출

- ObservableObject는 재사용될 수 있는 View에 직접 연결하면 위험

 

3. 올바른 패턴 예시

✅ 예시 1: View 내부에서 생성해서 유지 → @StateObject

struct ProfileView: View {
    @StateObject private var vm = ProfileViewModel()   // ✅ 한 번만 생성됨

    var body: some View {
        Text(vm.name)
    }
}

장점

- ViewModel이 화면 생명주기 동안 유지됨

- API 중복 호출 없음

- 데이터 손실 없음

✅ 예시 2: 부모에서 생성해 자식이 사용만 할 때 → @ObservedObject

struct ParentView: View {
    @StateObject private var vm = ParentViewModel()

    var body: some View {
        ChildView(vm: vm)   // 자식은 상태를 “관찰만” 함
    }
}

struct ChildView: View {
    @ObservedObject var vm: ParentViewModel   // 부모에서 주입받음
    var body: some View {
        Text(vm.title)
    }
}

장점

- ViewModel 소유권을 부모가 가지므로 불필요한 초기화 방지

- 자식은 vm을 관찰만 하므로 안전

✅ 예시 3: NavigationStack / TabView 안에서 ViewModel 소유권 유지

struct RootView: View {
    @StateObject private var vm = HomeViewModel()   // 여기서 유지해야 함

    var body: some View {
        NavigationStack {
            HomeView(vm: vm)
        }
    }
}

장점

- 화면 이동 시 ViewModel이 재생성되지 않음

- navigation push/pop 시 상태 유지 가능 

 

4. 실전 적용 팁

✔ 팁 1 – ViewModel을 “생성하는 곳”과 “사용하는 곳”을 명확히 분리

  • 생성 → @StateObject
  • 사용 → @ObservedObject

✔ 팁 2 – ViewModel을 자식에 전달할 때는 ObservableObject를 무조건 주입

초기화를 자식에서 하지 않도록 한다.

✔ 팁 3 – List/ScrollView 셀 내부에서 ObservableObject를 직접 가지지 말 것

셀 재사용 패턴과 충돌해 load()가 반복된다.

✔ 팁 4 – NavigationStack은 부모 단에서 ViewModel을 소유해야 한다

push/pop마다 ViewModel이 재생성되는 문제 방지.

✔ 팁 5 – @StateObject는 반드시 private으로 보호

외부에서 상태를 덮어쓰는 것을 예방.

 

5. 정리

  • @StateObject는 “이 View가 생명주기를 소유하는 ViewModel”을 의미한다.
  • @ObservedObject는 “부모가 관리하는 ViewModel을 관찰하는 것”에 사용한다.
  • 잘못 선택하면 ViewModel 초기화가 반복되며 API 중복 호출 같은 문제가 발생한다.
  • NavigationStack, 리스트, 탭 구조에서는 올바른 소유권 관리가 필수이다.
  • 이 원칙만 제대로 지켜도 SwiftUI 상태 관리 문제의 절반은 해결된다.
반응형
Posted by 까칠코더
,