@StateObject는 SwiftUI에서 ObservableObject 인스턴스를 view내에서 직접 생성하고 소유하기 위한 Property Wrapper입니다. SwiftUI는 객체가 view의 생명 주기맹 당 한 번 초기화되고, 재렌더링시 재생성되지 않도록 보장합니다. Apple Developer Documentation (2025)에 따르면, @StateObject는 데이터 소스를 생성하는 루트 view에 권장됩니다. @StateObject는 SwiftUI 계층구조에서 ObservableObject를 소유하는 올바른 선택입니다.
주요 포인트
@StateObject는 SwiftUI 2.0(iOS 14)에서 도입된 Property Wrapper로, @ObservedObject와 @State의 기능을 결합한 것입니다. @ObservedObject와 마찬가지로 ObservableObject의 변경 사항을 구독합니다. @State와 마찬가지로, 데이터가 반복되는 view 구조 초기화에서도 유지되도록 보장합니다. @StateObject는 view가 처음 화면에 나타났을 때 객체를 한 번 생성하고 SwiftUI의 heap에 저장합니다.
@StateObject 이전에는 개발자가 view에서 생성된 것을 포함한 모든 ObservableObject에 @ObservedObject를 사용했습니다. 이로 인해 부모 view가 업데이트될 때 빈번한 데이터 손실이 발생하여 view 구조가 재생성되고 @ObservedObject 인스턴스가 함께 사라졌습니다. @StateObject는 안정성 보장을 추가하여 이 문제를 해결했습니다.
기본 규칙: @StateObject는 기본 초기화자(let model = ViewModel())에서 객체를 생성하는 view에서 사용됩니다. 이 객체를 받는 자식 view는 @ObservedObject를 사용합니다. 이 분리를 통해 전체 계층에서 단일 진실의 원천이 보장됩니다.
SwiftUI는 @State와 유사한 저장소 관리자를 통해 @StateObject의 생명 주기를 관리합니다. view가 처음 나타나면 SwiftUI는 객체에 메모리를 할당하고 이를 영구 영역에 저장합니다. 후속 재렌더링(body 호출)에서는 객체가 재생성되지 않고 기존 인스턴스가 사용됩니다. view가 계층에 있는 동안 객체는 살아 있습니다.
view가 계층에서 제거되면 SwiftUI는 @StateObject를 파괴하고 deinit을 호출합니다. view가 다시 계층에 추가되면 새 인스턴스가 생성됩니다. 설계시 이점을 고려하는 것이 중요합니다: view 제거 사이에 데이터를 보존해야 하는 경우 서비스 계층(싱글톤 또는 DI) 또는 @AppStorage를 사용하세요.
class TimerViewModel: ObservableObject {
@Published var seconds: Int = 0
private var timer: Timer?
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.seconds += 1
}
}
deinit {
timer?.invalidate()
}
}
struct TimerView: View {
@StateObject var viewModel = TimerViewModel()
var body: some View {
Text("\(viewModel.seconds)s")
.onAppear { viewModel.start() }
}
}
예시에서 TimerViewModel이 @StateObject를 통해 생성되며 TimerView가 화면에 있는 동안 살아 있습니다. 타이머는 onAppear에서 시작되고 deinit에서 정지됩니다. @ObservedObject를 사용한 경우 TimerView가 렌더말때마다 seconds = 0인 새 TimerViewModel이 생성되어 타이머가 제대로 동작하지 않습니다. @StateObject는 viewModel이 고유하고 안정적이도록 보장합니다.
@StateObject와 @ObservedObject 사이의 선택은 누가 객체를 소유하는가에 따라 달라집니다. view가 객체를 생성하는 경우 @StateObject입니다. view가 이미 생성된 객체를 받는 경우 @ObservedObject입니다. 이 규칙이 매우 중요하여 Xcode는 초기화자를 통해 객체를 받는 자식 view에서 @StateObject를 사용할 때 경고를 표시합니다.
| 상황 | 추천 Wrapper |
|---|---|
| view가 ViewModel()으로 모델 생성 | @StateObject |
| view가 부모로부터 모델 받음 | @ObservedObject |
| 모델이 한 view에서 사용 | @StateObject |
| 모델이 Environment를 통해 전달 | @EnvironmentObject |
| 미리보기에 모델 필요 | @ObservedObject + mock |
실습에서는 프로젝트 초기에 @StateObject를 루트 view에, @ObservedObject를 모든 자식 view에 사용하는 경우가 많습니다. 애플리케이션이 성장함에 따라 일부 @StateObject 인스턴스는 계층을 간소화하기 위해 @EnvironmentObject로 대체할 수 있습니다. 그러나 @StateObject는 자체 로직이 있는 모듈형 화면에 여전히 가장 좋은 선택입니다.
첫 번째 패턴—@StateObject를 통한 MVVM. ObservableObject로서의 ViewModel이 @StateObject를 통해 view에서 생성됩니다. ViewModel은 @Published 속성과 비즈니스 로직을 포함합니다. view는 변경 사항을 구독하고 인터페이스를 업데이트합니다. 이 접법은 테스트 가능한 격리를 제공합니다: ViewModel은 인스턴스를 직접 생성하여 UI 없이 테스트할 수 있습니다.
둘 번째 패턴—의존성이 있는 @StateObject. ViewModel에 서비스가 필요한 경우 파라미터를 통한 초기화를 사용하세요. 예: @StateObject var viewModel = UserViewModel(api: APIClient.shared). 단, 주의하세요: 파라미터는 모든 body 렌더링에서 계산되지만 객체는 한 번만 생성됩니다. SwiftUI는 @StateObject의 후속 초기화를 무시합니다.
셋 번째 패턴—중첩 @StateObject. SwiftUI에서는 한 view에 여러 @StateObject를 가질 수 있지만 그렇게 할 경우는 그리 많지 않습니다. 보통 하나의 @StateObject가 view의 전체 데이터 세트를 관리합니다. 로직이 너무 복잡해지면 하나의 @StateObject 내에서 @ObservedObject 서비스의 조합으로 분할하세요.
struct AppView: View {
@StateObject var router = NavigationRouter()
@StateObject var auth = AuthViewModel()
var body: some View {
ContentView()
.environmentObject(router)
.environmentObject(auth)
}
}
예시에서 AppView는 두 개의 @StateObject를 생성합니다: 네비게이션 관리를 위한 NavigationRouter와 인증을 위한 AuthViewModel. 두 객체 모두 environmentObject를 통해 Environment에 주입됩니다. 어떤 자식 view도 초기화자 체인을 거치지 않고 @EnvironmentObject를 통해 액세스할 수 있습니다.
@StateObject는 임의의 파라미터로 초기화를 지원하지만 중요한 유의사항이 있습니다: 초기화자는 한 번만 호출됩니다. 후속 body 재렌더링에서 새 파라미터 값은 무시됩니다. 따라서 @State var id: Int = 5를 @StateObject var vm = ViewModel(id: id)에 전달하면 id가 변경되어도 ViewModel은 새 값을 받지 못합니다.
이 문제를 해결하려면 동기화를 위해 onReceive 또는 onAppear를 사용하세요. Combine을 통해 ViewModel 내에서 파라미터 변경을 구독하거나 view 레벨에서 .onChange(of:) _방법으로 파라미터를 전달하세요. 대안으로 객체가 외부 변경에 동적으로 응답해야 하는 경우 @StateObject 대신 @ObservedObject를 사용하세요.
struct DetailView: View {
let itemId: Int
@StateObject var viewModel = DetailViewModel()
var body: some View {
Text(viewModel.title)
.onAppear { viewModel.load(id: itemId) }
}
}
올바른 접법: DetailView는 itemId를 let 속성으로 받고(구조체의 초기화자를 통해 전달), @StateObject는 파라미터 없이 DetailViewModel을 생성합니다. onAppear에서 전달받은 ID에 대한 데이터를 로드하기 위해 load(id:) 메소드가 호출됩니다. 이를 통해 ViewModel은 @StateObject 메카니즘에 의해 생성되지만 데이터는 현재 ID로 view가 표시될 때마다 로드됩니다.
주요 실수는 부모로부터 객체를 받는 자식 view에서 @StateObject 사용하는 것입니다. ParentView가 @StateObject model을 생성하고 ChildView가 @StateObject var model: ModelType(기본 파라미터 사용)을 선언하면 ChildView는 자체 독립적인 인스턴스를 생성합니다. 부모와 자식 객체는 연결되지 않고 한 객체의 변경이 다른 객체에 반영되지 않습니다.
둘 번째 실수는 List 또는 ForEach에 @StateObject 배치 하는 것입니다. 목록의 각 요소가 자체 @StateObject를 생성하여 여러 독립 인스턴스가 만들어집니다. 목록의 경우 올바른 접법은 @ObservedObject를 통해 모든 요소에 하나의 ObservableObject를 전달하거나 List 내에서 @State와 함께 Identifiable 구조체를 사용하는 것입니다.
셋 번째 문제는 deinit 정리 부재입니다. @StateObject는 view 생명 주기 전체를 통해 살아 있습니다. 객체가 타이머, Combine 구독 또는 네트워크 요청을 생성하는 경우 deinit이 그들을 취소해야 합니다. 그렇지 않으면 메모리 누수와 화면 닫힌 후 백그라운드 작업 지속이 불가피합니다. 항상 Combine Cancellable 저장소를 사용하거나 deinit에서 타이머를 무효화하세요.
자주 묻는 질문
@StateObject는 WWDC 2020에서 SwiftUI 2.0에 추가되었으며 iOS 14, macOS 11, watchOS 7, tvOS 14와 함께 리리스되었습니다. 그 이전에는 @ObservedObject가 ObservableObject를 작업하는 유일한 방법이었으며, 이로 인해 빈번한 데이터 손실 버그가 발생했습니다.
아니요, @StateObject는 Optional 유형을 지원하지 않습니다. 객체는 선언 시 초기화되어야 합니다. 옵션 객체가 필요한 경우 옵션 유형으로 @ObservedObject 또는 @EnvironmentObject를 사용하세요.
ObservableObject의 초기화자와 deinit에 print(#function)을 추가하세요. 재렌더링시 init이 호출되지 않으면 @StateObject가 올바르게 작동하는 것입니다. 만약 init이 매번 호출된다면 @ObservedObject를 @StateObject로 바꾸세요.
네, @StateObject는 UIHostingController를 통해 UIKit에 임베디된 SwiftUI view에서 작동합니다. 객체 생명 주기는 UIViewController가 아닌 SwiftUI view에 연결됩니다. SwiftUI view가 대체되면 @StateObject는 파괴됩니다.
직문이 분리된 여러 작은 @StateObjects가 좋습니다. 이는 테스트 가능성, 재사용성 및 성능을 향상시킵니다. 한 객체가 변경되면 전체 view가 아닌 구독 중인 일부 인터페이스만 재그리멀되는 방식입니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.