🇺🇸 EN

👁️ @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 불필요
✅ 타입 안전성 향상
✅ 컴파일 타임에 에러 검출

📦 학습 자료

💻
GitHub 프로젝트
🍎
Apple 공식 문서
🎥
WWDC23 세션

📎 Apple 공식 자료

📘 공식 문서 🎬 WWDC 세션