250x250
반응형
Notice
Recent Posts
Recent Comments
Link
관리 메뉴

스윞한 개발자

@Observable 매크로의 내부 동작과 ObservableObject와의 비교 본문

Swift 이론

@Observable 매크로의 내부 동작과 ObservableObject와의 비교

스윞남 2025. 5. 4. 21:26
728x90
반응형
SMALL

안녕하세요! 

또 새로운 주제로 포스팅을 해보려고 합니다! 오늘의 주제는 "@Observable 매크로의 내부 동작과 ObservableObject와의 비교"입니다!

 

 

오늘 포스팅에서는 WWDC23에서 소개되고 iOS17부터 본격적으로 사용할 수 있게 된 @Observable 매크로에 대해 파해쳐보겠습니다!

 

# ObservableObject의 한계

 

SwiftUI가 처음 등장했을 때, 상태 관리를 위해 ObservableObject 프로토콜과 @Published 속성 래퍼를 사용했습니다.

저 또한 아직까지 뷰모델에서 ObservableObject와 @Published를 자주 사용하는 거 같습니다.

 

ObservableObject의 특징을 보면

1. ObservableObject는 참조 타입인 클래스에서만 사용이 가능했습니다.

2. 객체 수준에서만 관찰이 이루어져 특정 프로퍼티 변경만 추적하기 어려웠습니다.

3. @Published, @ObservableObject, @StateObject 등 다양한 속성 래퍼를 상황에 맞게 사용했어야 했습니다.

4. 변경 여부와 관계없이 객체 전체를 구독하면 불필요한 뷰 렌더링이 발생할 수 있었습니다.

 

위와 같은 불편함이 있었고,,, 그래서 나오게된!! 

 

# @Observable 매크로

 

iOS 17과 Swift5.9에 등장한 @Observable 매크로는 기존의 ObservableObject의 한계를 보완하기 위해 새로 나오게 되었습니다!

 

@Observable
class UserModel {
    var name: String = "스윞남"
    var age: Int = 30
    
    func increaseAge() {
        age += 1
    }
}

struct ProfileView: View {
    var user = UserModel()
    
    var body: some View {
        VStack {
            Text("이름: \(user.name)")
            Text("나이: \(user.age)")
            Button("나이 증가") {
                user.increaseAge()
            }
        }
    }
}

 

기존에 뷰모델에서 ObservableObject, @Published, @ObservedObject, @StateObject 등 다양한 속성 래퍼들을 사용하는 것과 달리 한눈에 보기에도 코드가 엄청 간결해졌습니다!

 

하지만, 이렇게 간단한 코드 안에는.. 어려운 메커니즘들이 있습니다...

 

@Observable은 단순한 프로퍼티 래퍼가 아닌 매크로입니다. 컴파일 시점에서 코드를 생성하고 변환한다는 의미인데.. 이 말을 들으면 무슨 말인지 저는 이해가 안 됐습니다. 

 

우선, 매크로에 대해 정리해 보겠습니다.

 

## 매크로란?

컴파일 시점에 코드를 생성하거나 변환하는 도구입니다. 개발자가 작성한 짧은 코드를 컴파일러가 더 복잡한 코드로 확장시켜 주는? 코드 생성기라고 생각할 수 있습니다.

 

매크로에서도 두 가지 종류로 나뉩니다.

1. 선언 매크로 : 클래스, 구조체, 열거형 등 선언에 추가 코드를 생성합니다.

-> @Observable이 선언 매크로 유형입니다.

2. 표현식 매크로 : 특정 표현식을 다른 코드로 대체합니다.

 

매크로 동작 살펴보기

@Observable
class Counter {
    var count = 0
}

class Counter {
    var count = 0 {
        willSet {
            _$observationRegistrar.willSet(self, keyPath: \.count)
        }
    }
   
    private static let _$observationRegistrar = ObservationRegistrar<Counter>()
    
    func access<Member>(_ keyPath: KeyPath<Counter, Member>) {
        Counter._$observationRegistrar.access(self, keyPath: keyPath)
    }
}

 

우선 위의 과정은 컴파일 시간에 진행되기 때문에 런타임 오버헤드는 발생하지 않습니다. 위의 간단한 예시 코드에서 보면 저희는 간단하게 @Observable을 통해서 Counter를 사용할 수 있지만, 컴파일러가 실제로 처리하는 코드는 하위의 코드처럼 동작합니다.

 

@Observable 매크로의 내부 구조 

1. observationRegistrar

private static let _$observationRegistrar = ObservationRegistrar<Counter>()

모든 @Observable 타입은 내부적으로 ObservationRegistrar라는 타입의 인스턴스를 갖고 있습니다.

이는,

 - 프로퍼티 접근을 추적(어떤 뷰가 어떤 프로퍼티 사용하는가)

 - 변경 사항 구독자에게 알림

 - 구독자 - 프로퍼티 관계 관리

 

2. 프로퍼티 접근 추적 메커니즘

@Observable 매크로는 각 프로퍼티에 willSet 블록을 주입합니다. 

var count = 0 {
    willSet {
        _$observationRegistrar.willSet(self, keyPath: \.count)
    }
}

 

count 프로퍼티가 변경되려 할 때 Registrar에 알림을 보냅니다. Registrar는 이 프로퍼티를 구독하고 있는 모든 뷰에게 업데이트가 필요하다고 전달합니다.

 

3. 프로퍼티 접근 감지

SwiftUI뷰가 @Observable 객체의 프로퍼티에 접근할 때, 매크로에 의해 생성된 access 메서드가 호출됩니다,

func access<Member>(_ keyPath: KeyPath<Counter, Member>) {
    Counter._$observationRegistrar.access(self, keyPath: keyPath)
}

위의 access 메서드는 현재 실행 중인 뷰가 어떤 프로퍼티에 관심이 있는지 등록합니다. 위를 통해 어떤 뷰가 어떤 프로퍼티에 의존하는지 추적할 수 있게 됩니다.

 

4. 프로퍼티 변경 추적

프로퍼티 값이 실제로 변경될 때, Registrar는 해당 프로퍼티에 의존하는 뷰들에게만 선택적으로 프로퍼티가 변경되었음을 알립니다.

func withMutation<Member, T>(keyPath: KeyPath<Counter, Member>, _ mutation: () throws -> T) rethrows -> T {
    Counter._$observationRegistrar.willModify(self, keyPath: keyPath)
    let result = try mutation()
    return result
}

 

ObservableObject는 Combine 프레임워크의 objectWillChange 퍼블리셔를 사용합니다.

@Published 프로퍼티가 변경되면 객체 전체에 대한 변경 알림이 전송되고, 어떤 프로퍼티가 변경되었는지와 관계없이 객체 전체에 대한 구독이 발생했음을 의미합니다.

 

반면에, @Observable의 내부를 보았듯 프로퍼티별 추적 시스템을 사용합니다. 뷰가 렌더링 될 때 어떤 프로퍼티에 접근했는지 추적하고, 해당 프로퍼티가 변경될 때만 뷰를 업데이트합니다. 

 

# @Observable시 뷰의 렌더링 단계

struct UserView: View {
    var user = User()
    
    var body: some View {
        Text(user.name)
    }
}

 

1. @Observable타입인 User를 선언하고, 처음에 뷰는 user.name의 프로퍼티에 접근합니다. 

2. User 내부의 access 함수 -> user.access(\. name)을 호출하게 되고

3. Registrar는 UserView는 name 프로퍼티에 의존합니다.

Button("이름 변경") {
    user.name = "스윞남" 
}

 

또한 user.name 이 변경되려 할 때, 내부의 프로퍼티에 willSet 블록이 트리거가 되게 됩니다. Registrar는 name 프로퍼티에 의존하는 모든 뷰에게 업데이트가 필요함을 알리고 해당하는 뷰들만 다시 렌더링 하게 됩니다.

 

SwiftUI를 최근에 다시 공부하며 써보고 있는데... 버전이 올라가면 올라갈수록 기존의 것들이 금방 사라지고 바뀌는 거 같습니다.. 그리고 내부적으로는 어려운 내용도 많은 듯합니다..

 

오늘의 포스팅은 이 정도로 마무리하고 잘못된 내용에 대한 피드백은 언제든지 환영입니다!!

다음 포스팅에서는 더 유익한 SwiftUI에 대한 탐구로 돌아오겠습니다 ㅎㅎ

긴 글 읽어주셔서 감사합니다!

 

https://developer.apple.com/documentation/combine/observableobject

 

ObservableObject | Apple Developer Documentation

A type of object with a publisher that emits before the object has changed.

developer.apple.com

https://developer.apple.com/documentation/observation/observable()

 

Observable() | Apple Developer Documentation

Defines and implements conformance of the Observable protocol.

developer.apple.com

 

728x90
반응형
LIST

'Swift 이론' 카테고리의 다른 글

SwiftUI 뷰 렌더링 최적화  (0) 2025.04.24
Swift Opaque Type vs Swift Type Erasure  (0) 2025.04.14
DB Transaction / ACID  (0) 2025.03.17
Hot/Cold Observable, Multi/UniCast  (1) 2025.03.04
DAO, DTO, Entity + Repository Pattern  (2) 2025.02.17