@Observable 매크로

2025. 7. 3. 20:05·iOS/SwiftUI
반응형

👋 들어가기 전

 

매크로

👋 들어가기 전슬슬 새로운 개념들을 조금 씩 배워나가야할 것 같다.."매크로" 해야지 해야지 했는데 선 뜻 시작하기 힘들었던 개념인 것 같다. 이번 포스팅 역시 한번에 작성이 끝나지 않을 수

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 매크로는, 멤버의 저장 프로퍼티가 변경되면, 그 변경을 알린다.

 

두 가지 경우로 나뉜다.

  1. 계산 프로퍼티안에 있는 저장 프로퍼티가 변경되면, 해당 저장 프로퍼티 관련된 뷰를 업데이트
  2. 계산 프로퍼티 안에서 뷰 의존성이 있는 저장 프로퍼티를 사용하지 않으면 뷰 업데이터가 되지 않음

정리하면 계산 프로퍼티는 현재 뷰 의존성이 있는 저장 프로퍼티를 사용하지 않으면

뷰 업데이트 사이클에 자동으로 영향을 끼치지 않는다.

 

아래 코드 @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

https://developer.apple.com/documentation/swiftui/migrating-from-the-observable-object-protocol-to-the-observable-macro

 

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
'iOS/SwiftUI' 카테고리의 다른 글
  • 커스텀 SwipePopNavigationStack 구현하기
  • 커스텀 뷰를 만들 때 고민점
  • Custom Carousel 만들기
  • .scrollTargetLayout
Hamp
Hamp
남들에게 보여주기 부끄러운 잡다한 글을 적어 나가는 자칭 기술 블로그입니다.
  • Hamp
    Hamp의 분리수거함
    Hamp
  • 전체
    오늘
    어제
    • 분류 전체보기 (304)
      • CS (30)
        • 객체지향 (2)
        • Network (7)
        • OS (6)
        • 자료구조 (1)
        • LiveStreaming (3)
        • 이미지 (1)
        • 잡다한 질문 정리 (0)
        • Hardware (2)
        • 이론 (6)
        • 컴퓨터 그래픽스 (0)
      • Firebase (3)
      • Programing Langauge (37)
        • swift (32)
        • python (4)
        • Kotlin (1)
      • iOS (132)
        • UIKit (37)
        • Combine (1)
        • SwiftUI (32)
        • Framework (7)
        • Swift Concurrency (22)
        • Tuist (6)
        • Setting (11)
        • Modularization (1)
        • Instruments (6)
      • PS (59)
        • 프로그래머스 (24)
        • 백준 (13)
        • LeetCode (19)
        • 알고리즘 (3)
      • Git (18)
        • 명령어 (4)
        • 이론 (2)
        • hooks (1)
        • config (2)
        • action (7)
      • Shell Script (2)
      • Linux (6)
        • 명령어 (5)
      • Spring (13)
        • 어노테이션 (1)
        • 튜토리얼 (11)
      • CI-CD (4)
      • Android (0)
        • Jetpack Compose (0)
  • 블로그 메뉴

    • 홈
    • 태그
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    protocol
    concurrency
    Tuist
    GIT
    dfs
    CS
    dp
    백준
    dispatch
    lifecycle
    IOS
    AVFoundation
    Swift
    Spring
    property
    UIKit
    SwiftUI
    boostcamp
    투포인터
    프로그래머스
  • 최근 댓글

  • 최근 글

  • 반응형
  • hELLO· Designed By정상우.v4.10.0
Hamp
@Observable 매크로
상단으로

티스토리툴바