๐๏ธ @Observable ์ํ๊ด๋ฆฌ
iOS 17์ ์๋ก์ด Observation ํ๋ ์์ํฌ. ObservableObject๋ฅผ ์์ ํ ๋์ฒดํ๋ ํ๋์ ์ํ๊ด๋ฆฌ์ ๋๋ค.
โจ @Observable์ด๋?
iOS 17+์์ ๋์ ๋ Swift ๋งคํฌ๋ก ๊ธฐ๋ฐ ์ํ๊ด๋ฆฌ์ ๋๋ค. @Published, @StateObject, @ObservedObject๋ฅผ ๋์ฒดํ๋ฉฐ, ๋ ๊ฐ๋จํ๊ณ ์ฑ๋ฅ์ด ๋ฐ์ด๋ฉ๋๋ค.
๐ Before & After ๋น๊ต
// 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)") } }
// 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)") } }
๐ฏ ๊ธฐ๋ณธ ์ฌ์ฉ๋ฒ
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 ํตํฉ
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๊ฐ ํ์ ์์ต๋๋ค. ์ผ๋ฐ ํ๋กํผํฐ๋ก ์ ์ธํด๋ ์๋์ผ๋ก ๊ด์ฐฐ๋ฉ๋๋ค.
struct ContentView: View { // โ ๋ถํ์ (iOS 17+) // @State private var viewModel = TodoViewModel() // โ ์ด๋ ๊ฒ๋ง ํด๋ ์๋์ผ๋ก ์ ๋ฐ์ดํธ var viewModel = TodoViewModel() var body: some View { Text("\(viewModel.count)") } }
๐ฏ @ObservationIgnored
ํน์ ํ๋กํผํฐ๋ฅผ ๊ด์ฐฐ ๋์์์ ์ ์ธํ ์ ์์ต๋๋ค.
import Observation @Observable class ViewModel { var count = 0 // ๊ด์ฐฐ๋จ (UI ์ ๋ฐ์ดํธ) @ObservationIgnored var internalCache: [String: Any] = [:] // ๊ด์ฐฐ ์ ๋จ (์ฑ๋ฅ ์ต์ ํ) func increment() { count += 1 internalCache["lastIncrement"] = Date() // UI ์ ๋ฐ์ดํธ ์ ํจ } }
๐ Environment์์ ์ฌ์ฉ
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
์ฐ์ฐ ํ๋กํผํฐ๋ ์๋์ผ๋ก ๊ด์ฐฐ๋ฉ๋๋ค.
@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์ ์ค์ ๋ก ์ฌ์ฉํ๋ ํ๋กํผํฐ๋ง ๊ด์ฐฐํฉ๋๋ค.
@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 ๋ฐ์์๋ ๋ณ๊ฒฝ ๊ฐ์ง๊ฐ ๊ฐ๋ฅํฉ๋๋ค.
import Observation @Observable class Counter { var value = 0 } let counter = Counter() // ๋ณ๊ฒฝ ๊ฐ์ง withObservationTracking { print("ํ์ฌ ๊ฐ: \(counter.value)") } onChange: { print("๊ฐ์ด ๋ณ๊ฒฝ๋์์ต๋๋ค!") } counter.value = 10 // onChange ์คํ๋จ
๐ Migration ๊ฐ์ด๋
// 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 ๋ถํ์
โ
ํ์
์์ ์ฑ ํฅ์
โ
์ปดํ์ผ ํ์์ ์๋ฌ ๊ฒ์ถ