iOS 17의 @Observable 매크로: SwiftUI 성능 최적화와 상태 관리 간소화

0

SwiftUI 렌더링 개선

목록 보기
8/8

서론

SwiftUI에서는 Dynamic property의 상태를 변경하여 View를 업데이트합니다.
이를 최적화하기 위해 각 Dynamic property를 별도의 View로 모듈화하거나
@StateObject 또는 @ObservedObject를 사용해 ViewModel을 파편화하여 불필요한 업데이트를 최소화하려는 노력이 필요했습니다.

그러나 두 가지 이상의 여러 View를 동시에 업데이트해야 하는 상황이 많아지면서, ViewModel 파편화에는 한계가 드러나기 시작합니다.
특히 @Published를 기준으로 ViewModel을 지나치게 분리하면 코드의 복잡성이 급격히 증가하고, 가독성이 심각하게 저해될 수 있습니다.
반면, ViewModel을 분리하지 않고 하나로 통합하면 Main Thread에서 불필요한 계산 작업이 늘어나 프레임 드랍과 같은 성능 문제가 발생할 수 있습니다.

이러한 문제를 해결하기 위해, iOS 17에서 @Observable 매크로가 도입되었습니다.
이를 통해 ViewModel을 분리하지 않고도 필요한 부분만 효율적으로 업데이트할 수 있어, 코드 가독성을 유지하면서도 성능 최적화를 달성할 수 있습니다.

이 글에서는 @Observable 매크로의 동작 원리와 활용 방법, 그리고 이를 사용해야 하는 이유에 대해 알아보겠습니다.


@Observable이란?

https://developer.apple.com/documentation/Observation

@Observable은 내부적으로 컴파일러가 상태 변화를 자동으로 추적하고, SwiftUI View에서 이를 반영할 수 있는 코드를 생성해 줍니다.
이를 통해 상태 관리를 위한 추가적인 설정 없이도 데이터와 UI 간의 동기화를 간단하게 구현할 수 있습니다.
또한, @Published와 같은 반복적인 Boilerplate 코드를 작성하지 않아도 된다는 장점이 있습니다.

ViewModel에 @Observable을 사용하면 @StateObject를 사용하는 경우보다 코드가 훨씬 간결해지고, 관리가 용이해집니다.
복잡한 상태 관리 로직을 간소화하고, SwiftUI의 선언적 프로그래밍 스타일에 더욱 부합합니다.

@StateObject@Observable

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 마다 분리하지 않아도 되고, 코드의 가독성도 높아지며 유지보수도 더 쉬워지는 장점이 있습니다.


Observable의 추가 이점

1. Model이 Observable인 경우 ViewModel은 Observable이 아니어도 됩니다.

final class ViewModel {
    let model = Model()
    
    func upCount() {
        model.upNumber()
    }
}

@Observable final class Model {
    private(set) var number: Int = 0
    
    func upNumber() {
        number += 1
    }
}

2. Model이 Observable인 경우 ViewModel을 struct로 참조할 수 있습니다.

struct ViewModel {
    let model = Model()
    
    func upCount() {
        model.upNumber()
    }
}

@Observable final class Model {
    private(set) var number: Int = 0
    
    func upNumber() {
        number += 1
    }
}

3. ViewModel을 let으로 참조할 수 있습니다.

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()
        }
    }
}

class가 아닌 객체에도 적용이 가능한가요?

struct는 값을 복사하여 전달하기 때문에, 상태 추적을 위해 참조가 필요한 @Observable의 특성과 맞지 않습니다.

actor는 데이터를 안전하게 처리하기 위해 비동기 환경에서 동작합니다.
하지만 @Observable은 상태 변화에 동기적으로 반응하며, View를 즉시 업데이트합니다.
이 때문에 actor는 @Observable에 적합하지 않습니다.

따라서, @Observable은 class에서만 사용 가능합니다.


결론

@Observable 매크로는 iOS 17에서 도입된 강력한 상태 관리 도구로, SwiftUI의 View 업데이트를 보다 효율적으로 처리할 수 있게 해줍니다.
이를 통해 기존의 @StateObject@ObservedObject를 대체하며, ViewModel을 복잡하게 파편화하지 않고도 불필요한 렌더링을 줄여 성능을 최적화할 수 있습니다.
또한, @Observable을 사용하면 코드의 가독성도 높아지고 유지보수가 용이해지며, 상태 변화에 따른 View 업데이트를 자동으로 관리할 수 있습니다.

그러나 @Observable은 class에서만 사용할 수 있기 때문에, 값 타입인 struct나 비동기 환경에서 동작하는 actor와는 호환되지 않는다는 점을 염두에 두어야 합니다.

profile
https://github.com/sustainable-git

0개의 댓글

관련 채용 정보