
👋 들어가기 전
매크로
👋 들어가기 전슬슬 새로운 개념들을 조금 씩 배워나가야할 것 같다.."매크로" 해야지 해야지 했는데 선 뜻 시작하기 힘들었던 개념인 것 같다. 이번 포스팅 역시 한번에 작성이 끝나지 않을 수
hamp.tistory.com
이전에 학습한 매크로 내용은 사실 @Observable 매크로를 학습하기 위함이었다..

SwiftUI를 잠시 놓고 있었는데, Apple에서 SwiftUI를 위한 너무 좋은 기능을 만들어줬다.
이번 기회에 한번 정리하고 가자.
역시 기본은 WWDC로 설명을 이어나가보자.
🏁 학습할 내용
- What is Observation?
- SwiftUI property wrappers
- Advanced uses
- Before and After
❓What is Observation?
매크로 해석
먼저 macro로 포스팅에서 공부했던 지식을 토대로 한번 정의를 macro로 문법을 먼저 해석해보자.
먼저 조금 쉽게 해석하기 위해, 줄바꿈과 들여쓰기가 임의로 들어갔다.
@attached(member,
names:
named(_$observationRegistrar),
named(access),
named(withMutation),
named(shouldNotifyObservers))
@attached(memberAttribute)
@attached(extension, conformances: Observable)
macro Observable()
첫번 째로 @attached 매크로 중, member를 사용하며, 매크로로 확장되는 멤버는 다음과 같다.
- _$observationRegistrar
- ObservationRegistrar 타입, Observer를 등록하곡 관리하는 내부 저장소 역할
- access
- access<Subject, Member> 타입, View가 어떤 속성을 읽고 있는 지 추적
- withMutation
- withMutation<Subject, Member, T> 타입,Observer에 등록된 수정을 식별함,
- 수정 전 willSet 호출
- 수정 후 didSet 호출
- shouldNotifyObservers
- 특정 속성 값 변경 시, 기존 값과 비교해서 알림을 보낼지 판단
public struct ObservationRegistrar : Sendable {
/// Creates an instance of the observation registrar.
///
/// You don't need to create an instance of
/// ``Observation/ObservationRegistrar`` when using the
/// ``Observation/Observable()`` macro to indicate observably
/// of a type.
public init()
/// Registers access to a specific property for observation.
///
/// - Parameters:
/// - subject: An instance of an observable type.
/// - keyPath: The key path of an observed property.
public func access<Subject, Member>(_ subject: Subject, keyPath: KeyPath<Subject, Member>) where Subject : Observable
/// A property observation called before setting the value of the subject.
///
/// - Parameters:
/// - subject: An instance of an observable type.
/// - keyPath: The key path of an observed property.
public func willSet<Subject, Member>(_ subject: Subject, keyPath: KeyPath<Subject, Member>) where Subject : Observable
/// A property observation called after setting the value of the subject.
///
/// - Parameters:
/// - subject: An instance of an observable type.
/// - keyPath: The key path of an observed property.
public func didSet<Subject, Member>(_ subject: Subject, keyPath: KeyPath<Subject, Member>) where Subject : Observable
/// Identifies mutations to the transactions registered for observers.
///
/// This method calls ``willset(_:keypath:)`` before the mutation. Then it
/// calls ``didset(_:keypath:)`` after the mutation.
/// - Parameters:
/// - of: An instance of an observable type.
/// - keyPath: The key path of an observed property.
public func withMutation<Subject, Member, T>(of subject: Subject, keyPath: KeyPath<Subject, Member>, _ mutation: () throws -> T) rethrows -> T where Subject : Observable
}
두번 째는 @attached 매크로 중, memberAttribute로 각 멤버들에게 명시된 attribute를 일괄적으로 부착한다.
세번 째는 @attached 매크로 중, extension으로 Observable 프로토콜을 채택하게 만든다.
🌀 SwiftUI property wrappers
Observable 매크로가 등장하면서 이전의 Property Wrapper를 대체하는
새로운 Property Wrapper를 알아보자.
1️⃣ @State
@State는 이전에도 있었지만 @Observable을 통해 사라진 프로퍼티는 사실
@StateObject이다.
@Observable은 직접 초기화 및 상태 추적이 가능하므로 더 이상 @StateObject가 필요가 없다.
그런데 여기서 의문은 @State는 왜 여전히 필요할까?
역할
1. Observable이 하는 일: "속성의 변경 감지"
- @Observable은 타입 내부 속성의 변경을 추적
- Obesrvable 타입은 뷰가 인스턴스를 소유하고 있을 때 그 속성들의 변화를 알림
2. @State이 하는 일: "상태 유지"
- SwiftUI 입장에서 뷰는 매번 다시 만들어 짐
- 일반 프로퍼티는 매번 새로운 객체가 만들어져 관찰이 끊김
- @State property wrapper는 뷰의 생명주기 동안 상태 유지
As is and To be


2️⃣ @Environment
두번 째는 @Environment이다.
대체된 이유는 @StateObject와 동일하니 생략
As is and To be

3️⃣ @Bindable
세번 째는 @Bindable이다.
@ObservedObject을 대체힌다.
As is and To be

Apple에서 WWDC에서 소개해주는 property wrapper 선택 기준 표는 다음과 같다.

‼Advanced uses
수동 관찰
Acess와 withMutation
@Observable 매크로는, 멤버의 저장 프로퍼티가 변경되면, 그 변경을 알린다.
두 가지 경우로 나뉜다.
- 계산 프로퍼티안에 있는 저장 프로퍼티가 변경되면, 해당 저장 프로퍼티 관련된 뷰를 업데이트
- 계산 프로퍼티 안에서 뷰 의존성이 있는 저장 프로퍼티를 사용하지 않으면 뷰 업데이터가 되지 않음
정리하면 계산 프로퍼티는 현재 뷰 의존성이 있는 저장 프로퍼티를 사용하지 않으면
뷰 업데이트 사이클에 자동으로 영향을 끼치지 않는다.
아래 코드 @Observable 매크로를 통해 확장되는 acess와 withMutation 함수를 이용하여
어떻게 Obsevation 동기화가 가능케하는 지, 단편적으로 보여주는 코드이다.
@Observable class Donut {
var name: String {
get {
access(keyPath: \.name)
return someValue
}
set {
withMutation(keyPath: \.name) {
someValue.name = newValue
}
}
}
}
반대로 말하면, 위 방법을 쓰면 저장 프로피티와 상관 없는 계산 프로퍼티도 뷰 업데이트 사이클에
플래그를 던질 수 있다는 뜻
@ObservationTracked
또 다른 방법은 @ObservationTracked 매크로가 있다.
@attached(accessor, names: named(init), named(get), named(set), named(_modify))
@attached(peer, names: prefixed(`_`))
public macro ObservationTracked() = #externalMacro(module: "ObservationMacros", type: "ObservationTrackedMacro")
아래 코드에서 우리는 count라는 변수에 @ObservationTracked 매크로를 붙힌다.
import SwiftUI
@Observable
class ObservationViewModel {
@ObservationTracked var count: Int = 0
@ObservationIgnored var _count: Int
func countUp() {
count += 1
print("_count: \(_count)")
}
}
struct ContentView: View {
var viewModel = ObservationViewModel()
var body: some View {
Text("\(viewModel.count)")
Button("Click") {
viewModel.countUp()
}
}
}
결과를 살펴보면 버튼을 누를 때마다, count 변수가 증가하는데, _count까지 동기화가 진행된다.

이 때, 매크로 선언 쪽을 보면. 접근자 쪽에서 init, get , set , _modify 라는 놈들이 추가되고
peer 매크로 확장으로 생성될 멤버는 prefixed(_)로 선언됐기 때문에, _count 변수가 생성된다.
그런데 중요한 내용은 _count는 접근자 (get, set)에서 사용내에서 사용하기 때문에
(매크로 입장에서는 따로 밖에서 선언한다고 확신함)
@ObservationTracked를 사용할 때는 반드시 내가 직접 _count를 정의해줘야한다.
_count를 주석처리하면 다음과 같은 오류가 나옴

위 확장된 매크로로 확장된 결과를 차근 분석해보자.

_count값이 초기화 되지 않았는데도 컴파일 에러가 나지 않는 이유는
매크로를 통해 확장된 코드에서 초기화 해주기 떄문이다.
@storageRestrictions(initializes: _count)
init(initialValue) {
_count = initialValue
}
// count의 초기 값을 _count라는 실제 저장 변수에 저장
// @storageRestrictions는 Swift 컴파일러에게 이 init이 _count를 초기화한다고 알려준다.
get {
access(keyPath: \.count)
return _count
}
// access(keyPath:)를 호출하여 읽기 접근을 추적 (SwiftUI가 어떤 값이 사용되는지 추적)
// _count 값을 리턴
set {
withMutation(keyPath: \.count) {
_count = newValue
}
}
// withMutation(keyPath:)를 호출하여 변경을 추적합니다.
// 그 안에서 _count 값을 실제로 변경
// 이를 통해 SwiftUI가 변경 사실을 감지
_modify {
access(keyPath: \.count)
_$observationRegistrar.willSet(self, keyPath: \.count)
defer {
_$observationRegistrar.didSet(self, keyPath: \.count)
}
yield &_count
}
_modify는 inout 접근을 지원할 때 사용 (in place 수정)
yield &_count로 inout으로 _count를 직접 조작할 수 있게 한다. (호출자에게 주소를 넘겨 수정할 기회를 줌)
하지만 그 전후로 willSet, didSet을 호출해서 SwiftUI의 관찰 시스템에 값이 바뀔 거라는 사실을 알려줌
관찰 무시
@ObservationIgnored 매크로를 사용하면 뷰 업데이트를 무시할 수 있다.
import SwiftUI
@Observable
class Counter {
@ObservationIgnored
var message = "Start"
func updateMessage() {
message = "Updated to message"
print("🍯 \(message)")
}
}
struct ContentView: View {
@State private var counter = Counter()
var body: some View {
VStack {
Text("Message: \(counter.message)") // @ObservationTracked 필요
Button("Increment") {
counter.updateMessage()
}
}
.padding()
}
}
message 값이 변경되어도 뷰 업데이트가 진행되지 않는다.

⏰ Before and After
기존 코드와 @Observable 매크로를 사용했을 때 가장 크게 달라지는 점들을 정리해보자.
🧹 간결해진 코드
- Model
- ObservableObject 프로토콜 채택 필요 ❌
- @Published Property Wrapper 사용 ❌
- View
- @ObservedObject Property Wrapper 사용 ❌

💉 효율적인 뷰 업데이트 매커니즘
뷰에서 사용하지 않는 프로퍼티 변경으로 인한 뷰 업데이트가 되는 사이드 이팩트가 사라짐
무조건 뷰의 body 프로퍼티에서 직접 읽은 프로퍼티 갱신에만 뷰 업데이트 사이클이 돌아간다.
아래 코드를 보면 LibraryView의 body에 대한 직접적인 의존성은 books지, book의 title이 아니다.
struct LibraryView: View {
@State private var books = [Book(), Book(), Book()]
var body: some View {
List(books) { book in
Text(book.title)
}
}
}
그래서 title에 대한 의존성은 Text가 갖고있다.
그렇기 때문에 title이 변하면 Text가 갱신되는거 지, List가 갱신되지 않는다.
🫙optional 형태의 data model 객체 사용 가능
이전에 @ObservedObject property wraaper를 사용하면 제네릭 타입 추론 문제가 있어
컴파일 에러가 발생
하지만 @Observable 매크롤 쓰면, 옵셔널 형태로도 가능하다.

📂 collection 형태의 data model 객체 사용 가능

✅ property wrapper 매칭 최종 정리
| ObservableObject | @Observable |
| @StateObject | @State |
| @ObservableObject | @Bindable |
| @EnvironmentObject | @Environment |
출처
https://developer.apple.com/kr/videos/play/wwdc2023/10149/
SwiftUI의 Observation 알아보기 - WWDC23 - 비디오 - Apple Developer
Observation을 통해 SwiftUI 데이터 모델을 단순화하세요. Observable 매크로는 모델을 단순화해 앱의 성능을 향상합니다. Observation과 매크로의 기초를 익히고 ObservableObject에서 Observable로...
developer.apple.com
https://eunjin3786.tistory.com/580
[SwiftUI] @Observable 매크로 (1)
Swift 5.9 부터 Observable macro 를 사용할 수 있습니다. ✓ WWDC 23 > Discover Observation in SwiftUI ✓ Managing model data in your app / Migrating from the Observable Object protocol to the Observable macro ✓ Observation ✓ 구현 코드 WW
eunjin3786.tistory.com
https://developer.apple.com/documentation/observation/observable()
Observable() | Apple Developer Documentation
Defines and implements conformance of the Observable protocol.
developer.apple.com
Migrating from the Observable Object protocol to the Observable macro | Apple Developer Documentation
Update your existing app to leverage the benefits of Observation in Swift.
developer.apple.com
https://developer.apple.com/documentation/Observation/ObservationIgnored()
ObservationIgnored() | Apple Developer Documentation
Disables observation tracking of a property.
developer.apple.com
https://malchafrappuccino.tistory.com/176
[SwiftUI] @Observable, @ObservationTracked, @ObservationIgnored 알아보기
WWDC23에서 SwiftUI에 Observation 프레임워크가 새로 추가되었습니다. 이번 글에서는 @Observable 매크로 사용법과 추가적인@ObservationTracked, @ObservationIgnored 매크로의 사용법을 알아보겠습니다. Discover Obser
malchafrappuccino.tistory.com
https://jano.dev/apple/2024/12/10/Modify-and-Yield.html
Modify and Yield
Did you know Swift implementation has a _modify/yield property accessor?
jano.dev
https://github.com/swiftlang/swift-evolution/blob/main/proposals/0474-yielding-accessors.md
swift-evolution/proposals/0474-yielding-accessors.md at main · swiftlang/swift-evolution
This maintains proposals for changes and user-visible enhancements to the Swift Programming Language. - swiftlang/swift-evolution
github.com
'iOS > SwiftUI' 카테고리의 다른 글
| 커스텀 SwipePopNavigationStack 구현하기 (6) | 2025.07.13 |
|---|---|
| 커스텀 뷰를 만들 때 고민점 (0) | 2025.07.05 |
| Custom Carousel 만들기 (0) | 2025.03.16 |
| .scrollTargetLayout (0) | 2025.03.15 |
| textFiledStyle (0) | 2025.03.08 |