질문의 역사
Swift 5.9 이전에는 SwiftUI에서 반응형 프로그래밍이 ObservableObject 프로토콜과 Combine 프레임워크의 조합에 의존했습니다. 개발자들은 프로퍼티에 @Published로 주석을 달아 퍼블리셔를 합성하거나 objectWillChange.send()를 호출하여 뷰에 변화를 알렸습니다. 이 패턴은 조잡한 업데이트로 인한 문제를 겪었습니다. 모든 프로퍼티 변경이 전체 뷰 바디의 재평가를 유도했고, 이는 복잡한 뷰 모델을 위한 구조체 사용을 방해했습니다. Observation 프레임워크는 명시적인 퍼블리셔 선언 없이 세밀한 자동 반응성을 제공하기 위해 도입되었습니다.
문제
핵심 문제는 명시적 보일러플레이트 없이 프로퍼티 접근 및 변화를 감지하는 것으로, 타입 안전성과 높은 성능을 유지하는 것이었습니다. 전통적인 솔루션은 문자열 기반의 취약한 수동 키-값 관찰(KVO)을 요구하거나 도메인 모델을 어지럽히는 래퍼 타입을 사용해야 했습니다. 시스템은 런타임 오버헤드 없이 동적으로 종속성을 등록하기 위해 읽기 및 쓰기 작업을 가로채야 했습니다. ObservableObject의 참조 카운트 아이덴티티 전파의 구조적 제약 없이 말이죠.
해결책
Swift는 @Observable 매크로를 사용하여 동료, 메서드 및 접근자 매크로 기능을 결합합니다. 컴파일 중에 이 매크로는 주석이 달린 클래스를 변형하여 비공식적인 ObservationRegistrar 인스턴스를 주입합니다. 그 후 모든 저장된 프로퍼티를 계산된 접근자로 재작성하여 읽기에는 _$observationRegistrar.track(self, keyPath: ...), 쓰기에는 willSet/didSet 알림을 추가합니다. 이 확장은 자동으로 Observable 프로토콜에 대한 준수를 합성하고 필요한 observationRegistrar 계산 프로퍼티를 구현합니다. SwiftUI는 뷰 바디 평가 중 이 레지스트라와 통합되어 실제 접근된 프로퍼티에 대해서만 관찰자로 등록하므로, 수동 Combine 설정 없이도 세분화된 업데이트를 달성합니다.
@Observable class SettingsViewModel { var isDarkModeEnabled = false var notificationCount = 0 } // 개념적 컴파일러 확장: class SettingsViewModel { private let _$observationRegistrar = ObservationRegistrar() var isDarkModeEnabled: Bool { get { _$observationRegistrar.track(self, keyPath: \.isDarkModeEnabled) return _isDarkModeEnabled } set { _$observationRegistrar.willSet(self, keyPath: \.isDarkModeEnabled) let oldValue = _isDarkModeEnabled _isDarkModeEnabled = newValue _$observationRegistrar.didSet(self, keyPath: \.isDarkModeEnabled, oldValue: oldValue) } } private var _isDarkModeEnabled = false // ... notificationCount에 대한 동일한 패턴 }
당신은 실시간 금융 대시보드 SwiftUI 애플리케이션을 설계하고 있으며, 라이브 주식 가격, 사용자 포트폴리오 총액 및 뉴스 피드를 표시합니다. 뷰 모델에는 Boolean UI 플래그에서 복잡한 차트 데이터 구조에 이르기까지 30개 개별 프로퍼티가 포함되어 있습니다.
처음에 팀은 모든 프로퍼티에 @Published 래퍼를 사용하여 ObservableObject를 구현했습니다. 이로 인해 성능이 크게 저하되었습니다. 단일 주식 가격이 업데이트되면 전체 대시보드가 다시 계산되었습니다. 왜냐하면 ObservableObject가 객체 전체에서 "무언가가 변경되었다"고 알리기 때문에 세분화가 결여되었기 때문입니다. 코드도 장황하여 반복적인 @Published var 선언과 메모리 누수를 방지하기 위한 수동 AnyCancellable 저장이 필요했습니다.
팀은 성능 및 보일러플레이트 문제를 해결하기 위해 세 가지 아키텍처 접근 방식을 평가했습니다.
첫 번째 접근 방식은 수동 Combine 최적화였습니다. 그들은 각 중요한 프로퍼티에 대한 개별 PassthroughSubject 인스턴스를 생성하고 .onReceive를 사용하여 특정 업데이트에 구독했습니다. 장점은 어떤 UI 구성 요소가 새로 고쳐질지를 정밀하게 제어할 수 있다는 것이었습니다. 그러나 단점은 거대한 코드 증가였습니다. 30개의 주제가 필요하므로 30개의 구독과 오류가 발생하기 쉬운 수동 메모리 관리가 필요하여 코드base가 유지 관리 불가능해졌습니다.
두 번째 접근 방식은 SwiftUI의 @State를.value 타입 뷰 모델과 함께 사용하는 것이었습니다. 그들은 뷰 모델을 불변 값으로 보고 매 번 변할 때마다 교체하였습니다. 장점은 자연스러운 값의 의미와 자동 평등 체크가 중복 업데이트를 방지합니다. 단점은 참조 아이덴티티의 손실이었습니다. 모든 변형은 새로운 인스턴스를 생성하므로 ScrollView 위치 복원을 깨뜨리고 아이덴티티 상실로 인해 복잡한 객체 조정이 불가능하게 되었습니다.
세 번째 접근 방식은 @Observable 매크로를 채택했습니다. 클래스를 @Observable로 주석 붙이고 모든 @Published 속성을 제거함으로써, 컴파일러는 프로퍼티를 자동으로 ObservationRegistrar를 사용하도록 변환했습니다. 장점은 구문이 깨끗하게 유지되었으며, 단순한 var 선언으로 SwiftUI가 각 뷰의 바디에서 접근된 프로퍼티를 자동으로 추적하므로 특정 하위 뷰만 업데이트되었습니다. 단점은 Swift 5.9로 마이그레이션해야 하고 팀을 매크로 디버깅 기술에 대해 교육해야 한다는 것이었습니다.
팀은 세 번째 접근 방식을 선택했습니다. 이를 통해 200줄의 Combine 구독 코드를 제거하면서 세분화 문제를 해결했습니다. 그들은 주식 가격 업데이트가 빈번한 동안 CPU 사용량이 40% 감소하는 것을 관찰했습니다. 결과적으로 각 주식 가격 레이블이 독립적으로 업데이트되면서 정적 뉴스 피드 섹션에 대해 레이아웃 재계산을 트리거하지 않는 반응형 대시보드가 탄생했습니다.
왜 Observation 프레임워크는 관찰되는 타입이 클래스(참조 의미론)가 되어야 하며, @Observable가 구조체에 적용될 경우 어떤 컴파일 오류가 발생합니까?
@Observable 매크로는 저장된 프로퍼티로 ObservationRegistrar를 주입하고 Observable 프로토콜을 구현하는 방식으로 확장됩니다. 구조체는 값 타입으로 복사 시 쓰기 의미론을 가집니다. 각 변형은 개념적으로 고유한 아이덴티티를 가진 새로운 인스턴스를 생성합니다. ObservationRegistrar는 관찰자 목록 및 추적 범위와 같은 내부 상태를 유지해야 하며, 이 상태는 변형 동안 지속되어야 관찰 그래프를 유지할 수 있습니다. 구조체에 적용될 경우 변형이 상태를 올바르게 복사하게 되어 관찰자와 인스턴스 간의 연결이 끊기게 됩니다. 컴파일러는 이 요구사항을 충족하는 방식으로 값 타입에 필요한 저장된 프로퍼티를 추가할 수 없기 때문에 발생하는 오류를 생성합니다. 결과적으로, 이 타입은 필요로 하는 참조 안정성을 갖추지 못해 Observable에 준수할 수 없습니다.
Observation 프레임워크는 중첩된 관찰 가능한 객체를 어떻게 처리하며, 왜 @Published와 같이 개별 프로퍼티에 대해 예상 값($property)이 없는 것인가요?**
@Observable 클래스에 @Observable 클래스를 가진 프로퍼티가 포함되어 있는 경우, 프레임워크는 중첩된 객체에 대해 자동으로 재귀적으로 관찰하지 않고 프로퍼티 수준에서 접근을 추적합니다. outer.inner.name을 접근하면 외부 객체의 inner 프로퍼티에 대한 종속성을 등록합니다. inner 인스턴스가 완전히 대체되면 관찰자에게 알림이 전송됩니다. 그러나 inner.name에 대한 변경 사항은 외부 객체의 관찰자에게 알림을 전송하지 않으며, 외부 객체가 명시적으로 내부 객체를 추적해야만 알림이 전송됩니다. Combine과 달리, 개별 프로퍼티에 대한 Observation에는 예상 값 개념이 없습니다. 대신에 이 프레임워크는 퍼블리셔 스트림이 아닌 레지스트라를 통한 직접 프로퍼티 추적을 사용하기 때문입니다. SwiftUI에서 Observation의 $ 구문은 대신 @Bindable로 래핑된 전체 인스턴스에서 사용됩니다(예: @Bindable var viewModel: SettingsViewModel는 $viewModel.isDarkModeEnabled를 허용합니다).