👁️ @Observable 상태관리
iOS 17의 새로운 Observation 프레임워크. ObservableObject를 완전히 대체하는 현대적 상태관리입니다.
⭐ 난이도: ⭐
⏱️ 예상 시간: 1h
📂 App Frameworks
✨ @Observable이란?
iOS 17+에서 도입된 Swift 매크로 기반 상태관리입니다. @Published, @StateObject, @ObservedObject를 대체하며, 더 간단하고 성능이 뛰어납니다.
🆚 Before & After 비교
Before (ObservableObject).swift
// iOS 16 이하: ObservableObject class CounterViewModel: ObservableObject { @Published var count = 0 @Published var message = "" func increment() { count += 1 message = "Count: \(count)" } } struct ContentView: View { @StateObject private var viewModel = CounterViewModel() var body: some View { Text("\(viewModel.count)") } }
After (@Observable).swift
// iOS 17+: @Observable import Observation @Observable class CounterViewModel { var count = 0 var message = "" func increment() { count += 1 message = "Count: \(count)" } } struct ContentView: View { var viewModel = CounterViewModel() var body: some View { Text("\(viewModel.count)") } }
🎯 기본 사용법
TodoViewModel.swift
import Observation import Foundation @Observable class TodoViewModel { var todos: [Todo] = [] var isLoading = false var errorMessage: String? func addTodo(_ title: String) { let todo = Todo(title: title) todos.append(todo) } func loadTodos() async { isLoading = true defer { isLoading = false } do { // API 호출 try await Task.sleep(for: .seconds(1)) todos = [Todo(title: "샘플 할 일")] } catch { errorMessage = error.localizedDescription } } } struct Todo: Identifiable { let id = UUID() var title: String }
📱 SwiftUI 통합
ContentView.swift
import SwiftUI struct ContentView: View { var viewModel = TodoViewModel() var body: some View { NavigationStack { List(viewModel.todos) { todo in Text(todo.title) } .overlay { if viewModel.isLoading { ProgressView("로딩 중...") } } .navigationTitle("할 일") .toolbar { Button("추가") { viewModel.addTodo("새 할 일") } } .task { await viewModel.loadTodos() } } } }
🔄 @State vs 일반 프로퍼티
@Observable을 사용하면 @State가 필요 없습니다. 일반 프로퍼티로 선언해도 자동으로 관찰됩니다.
StateComparison.swift
struct ContentView: View { // ❌ 불필요 (iOS 17+) // @State private var viewModel = TodoViewModel() // ✅ 이렇게만 해도 자동으로 업데이트 var viewModel = TodoViewModel() var body: some View { Text("\(viewModel.count)") } }
🎯 @ObservationIgnored
특정 프로퍼티를 관찰 대상에서 제외할 수 있습니다.
IgnoredProperty.swift
import Observation @Observable class ViewModel { var count = 0 // 관찰됨 (UI 업데이트) @ObservationIgnored var internalCache: [String: Any] = [:] // 관찰 안 됨 (성능 최적화) func increment() { count += 1 internalCache["lastIncrement"] = Date() // UI 업데이트 안 함 } }
🔗 Environment에서 사용
EnvironmentUsage.swift
import SwiftUI @Observable class AppState { var isLoggedIn = false var username = "" } @main struct MyApp: App { @State private var appState = AppState() var body: some Scene { WindowGroup { ContentView() .environment(appState) } } } struct ContentView: View { @Environment(AppState.self) private var appState var body: some View { if appState.isLoggedIn { Text("안녕하세요, \(appState.username)") } else { LoginView() } } }
🧪 Computed Property
연산 프로퍼티도 자동으로 관찰됩니다.
ComputedProperty.swift
@Observable class CartViewModel { var items: [CartItem] = [] // 연산 프로퍼티도 자동으로 관찰됨 var totalPrice: Double { items.reduce(0) { $0 + $1.price } } var itemCount: Int { items.count } func addItem(_ item: CartItem) { items.append(item) // totalPrice와 itemCount가 자동으로 업데이트됨 } } struct CartView: View { var viewModel = CartViewModel() var body: some View { VStack { Text("총 \(viewModel.itemCount)개") Text("합계: \(viewModel.totalPrice)원") } } }
⚡ 성능 최적화
@Observable은 실제로 사용하는 프로퍼티만 관찰합니다.
Performance.swift
@Observable class ViewModel { var name = "" var age = 0 var email = "" } struct NameView: View { var viewModel: ViewModel var body: some View { Text(viewModel.name) // name만 관찰, age/email 변경 시 업데이트 안 함 } } // ObservableObject는 @Published가 붙은 모든 프로퍼티 변경 시 업데이트 // @Observable은 실제로 body에서 사용하는 프로퍼티만 관찰 (더 효율적!)
🔄 withObservationTracking
SwiftUI 밖에서도 변경 감지가 가능합니다.
ManualTracking.swift
import Observation @Observable class Counter { var value = 0 } let counter = Counter() // 변경 감지 withObservationTracking { print("현재 값: \(counter.value)") } onChange: { print("값이 변경되었습니다!") } counter.value = 10 // onChange 실행됨
📊 Migration 가이드
Migration.swift
// BEFORE (iOS 13-16) class OldViewModel: ObservableObject { @Published var name = "" @Published var age = 0 } struct OldView: View { @StateObject var vm = OldViewModel() // 또는 @ObservedObject var vm: OldViewModel var body: some View { Text(vm.name) } } // AFTER (iOS 17+) @Observable class NewViewModel { var name = "" var age = 0 } struct NewView: View { var vm = NewViewModel() var body: some View { Text(vm.name) } } // 변경 사항 요약: // 1. ObservableObject → @Observable // 2. @Published 삭제 // 3. @StateObject/@ObservedObject → 일반 프로퍼티
💡 @Observable의 장점
✅ 코드가 80% 간결해짐
✅ 사용하는 프로퍼티만 관찰 (성능 향상)
✅ @Published, @StateObject 불필요
✅ 타입 안전성 향상
✅ 컴파일 타임에 에러 검출