SwiftUI에서는 Dynamic property의 상태를 변경하여 View를 업데이트합니다.
이를 최적화하기 위해 각 Dynamic property를 별도의 View로 모듈화하거나
@StateObject
또는 @ObservedObject
를 사용해 ViewModel을 파편화하여 불필요한 업데이트를 최소화하려는 노력이 필요했습니다.
그러나 두 가지 이상의 여러 View를 동시에 업데이트해야 하는 상황이 많아지면서, ViewModel 파편화에는 한계가 드러나기 시작합니다.
특히 @Published
를 기준으로 ViewModel을 지나치게 분리하면 코드의 복잡성이 급격히 증가하고, 가독성이 심각하게 저해될 수 있습니다.
반면, ViewModel을 분리하지 않고 하나로 통합하면 Main Thread에서 불필요한 계산 작업이 늘어나 프레임 드랍과 같은 성능 문제가 발생할 수 있습니다.
이러한 문제를 해결하기 위해, iOS 17에서 @Observable
매크로가 도입되었습니다.
이를 통해 ViewModel을 분리하지 않고도 필요한 부분만 효율적으로 업데이트할 수 있어, 코드 가독성을 유지하면서도 성능 최적화를 달성할 수 있습니다.
이 글에서는 @Observable
매크로의 동작 원리와 활용 방법, 그리고 이를 사용해야 하는 이유에 대해 알아보겠습니다.
https://developer.apple.com/documentation/Observation
@Observable
은 내부적으로 컴파일러가 상태 변화를 자동으로 추적하고, SwiftUI View에서 이를 반영할 수 있는 코드를 생성해 줍니다.
이를 통해 상태 관리를 위한 추가적인 설정 없이도 데이터와 UI 간의 동기화를 간단하게 구현할 수 있습니다.
또한, @Published
와 같은 반복적인 Boilerplate 코드를 작성하지 않아도 된다는 장점이 있습니다.
ViewModel에 @Observable
을 사용하면 @StateObject
를 사용하는 경우보다 코드가 훨씬 간결해지고, 관리가 용이해집니다.
복잡한 상태 관리 로직을 간소화하고, SwiftUI의 선언적 프로그래밍 스타일에 더욱 부합합니다.
@StateObject | @Observable |
---|---|
![]() | ![]() |
@Observable
은 @StateObject
와 @ObservedObject
를 대체할 수 있는 상위 호환 기술입니다.
이를 사용하면 기존의 @StateObject
와 @ObservedObject
가 필요한 모든 상황에서 대체될 수 있으며, 성능 측면에서도 아래와 같은 이점을 제공합니다
예를 들어, ObservableObject를 채택하는 객체의 경우, 내부 @Published
값이 변경되면 값 변경과 직접 관련이 없는 View라도 해당 객체를 참조하고 있는 모든 View의 body가 다시 호출됩니다.
이는 불필요한 View 업데이트를 초래하며, 성능 저하의 원인이 됩니다.
https://velog.io/@sustainable-git/SwiftUI에서-불필요한-렌더링-제거하기2
import SwiftUI
final class ViewModel: ObservableObject {
@Published private(set) var count: Int = 0
func upCount() {
count += 1
}
}
struct ContentView: View {
@StateObject private var viewModel: ViewModel = .init()
var body: some View {
VStack {
let _ = Self._printChanges()
Button("count = \(viewModel.count)") {
viewModel.upCount()
}
AnotherView(viewModel: viewModel)
}
}
}
struct AnotherView: View {
@ObservedObject var viewModel: ViewModel
var body: some View {
let _ = Self._printChanges()
Text("나는 값이 안변하는데")
}
}
만약 ViewModel을 @Observable
로 변경하면, 상태가 변경된 property와 관련이 없는 View의 body는 호출되지 않습니다.
즉, 불필요한 View 렌더링을 방지하여 성능을 최적화할 수 있습니다.
import SwiftUI
@Observable final class ViewModel {
private(set) var count: Int = 0
func upCount() {
count += 1
}
}
struct ContentView: View {
private var viewModel: ViewModel = .init()
var body: some View {
VStack {
let _ = Self._printChanges()
Button("count = \(viewModel.count)") {
viewModel.upCount()
}
AnotherView(viewModel: viewModel)
}
}
}
struct AnotherView: View {
var viewModel: ViewModel
var body: some View {
let _ = Self._printChanges()
Text("나는 값이 안변하는데")
}
}
상태가 변경되는 property와 무관한 경우, body가 호출되지 않는다는 점은 ViewModel 내에 여러 속성을 추가해도 불필요한 렌더링이 발생하지 않는다는 의미입니다.
이로 인해 ViewModel을 @Published
마다 분리하지 않아도 되고, 코드의 가독성도 높아지며 유지보수도 더 쉬워지는 장점이 있습니다.
final class ViewModel {
let model = Model()
func upCount() {
model.upNumber()
}
}
@Observable final class Model {
private(set) var number: Int = 0
func upNumber() {
number += 1
}
}
struct ViewModel {
let model = Model()
func upCount() {
model.upNumber()
}
}
@Observable final class Model {
private(set) var number: Int = 0
func upNumber() {
number += 1
}
}
struct ViewModel {
let model = Model()
func upCount() {
model.upNumber()
}
}
@Observable final class Model {
private(set) var number: Int = 0
func upNumber() {
number += 1
}
}
struct ContentView: View {
let viewModel = ViewModel()
var body: some View {
Button("count = \(viewModel.model.number)") {
viewModel.upCount()
}
}
}
struct는 값을 복사하여 전달하기 때문에, 상태 추적을 위해 참조가 필요한 @Observable의 특성과 맞지 않습니다.
actor는 데이터를 안전하게 처리하기 위해 비동기 환경에서 동작합니다.
하지만 @Observable은 상태 변화에 동기적으로 반응하며, View를 즉시 업데이트합니다.
이 때문에 actor는 @Observable에 적합하지 않습니다.
따라서, @Observable은 class에서만 사용 가능합니다.
@Observable
매크로는 iOS 17에서 도입된 강력한 상태 관리 도구로, SwiftUI의 View 업데이트를 보다 효율적으로 처리할 수 있게 해줍니다.
이를 통해 기존의 @StateObject
나 @ObservedObject
를 대체하며, ViewModel을 복잡하게 파편화하지 않고도 불필요한 렌더링을 줄여 성능을 최적화할 수 있습니다.
또한, @Observable
을 사용하면 코드의 가독성도 높아지고 유지보수가 용이해지며, 상태 변화에 따른 View 업데이트를 자동으로 관리할 수 있습니다.
그러나 @Observable
은 class에서만 사용할 수 있기 때문에, 값 타입인 struct나 비동기 환경에서 동작하는 actor와는 호환되지 않는다는 점을 염두에 두어야 합니다.