ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [SE-0506] Advanced Observation Tracking
    Swift 2026. 2. 1. 06:46

    안녕하세요. 그린입니다 🍏

    이번 포스팅에서는 SE-0506 Advanced Observation Tracking에 대해 정리해보겠습니다 🙋🏻


    Intro

    Swift의 Observation 프레임워크는 @Observable 매크로로 간단하게 변화를 추적할 수 있게 해줍니다.

    하지만 기존 withObservationTracking은 비동기 환경에 최적화되어 있어서, 동기 시스템과 연동하거나 더 세밀한 제어가 필요한 경우에는 한계가 있었습니다.

     

    SE-0506은 이런 케이스를 위한 새로운 도구를 제공합니다.

    대부분의 개발자는 기존 @Observable과 Observations로 충분하지만, 미들웨어 인프라나 위젯 시스템을 개발하는 경우라면 이번 제안이 꼭 필요할 거예요 🚀

     

    현재 2026년 2월 3일까지 리뷰가 진행 중입니다!

     


    왜 필요한가?

    기존 Observation API는 비동기 관찰에는 좋지만, 동기 시스템과 연동할 때는 두 가지 큰 문제가 있습니다.

     

    문제 1: 이벤트 병합(coalescing)

    기존 withObservationTracking은 빠르게 연속으로 발생하는 이벤트를 병합할 수 있습니다.

    하지만 두 데이터 모델을 동기화하는 경우처럼, 즉각적이고 병합되지 않은 이벤트가 필요한 경우가 있죠.

     

    문제 2: willSet vs didSet

    기존 API는 "변화가 일어날 것"만 알려줍니다.

    하지만 값이 설정되기 인지 인지 구분해야 하는 경우도 있고, 객체가 deinit되는 시점을 알아야 하는 경우도 있습니다.

     

    문제 3: 연속 관찰

    UI 시스템 같은 경우, 비동기 컨텍스트 없이 계속해서 이벤트를 받아야 하는 경우가 있습니다.

     

    이런 케이스를 위한 솔루션이 바로 SE-0506입니다.

     

     


    제안된 해결책

    두 가지 새로운 메커니즘이 추가됩니다.

    1. 옵션을 받는 withObservationTracking: 언제/어떤 변화를 관찰할지 제어
    2. withContinuousObservationTracking: 이벤트 발생 후 자동으로 재관찰

    새로운 API

    옵션 기반 withObservationTracking

    public func withObservationTracking<Result: ~Copyable, Failure: Error>(
      options: ObservationTracking.Options,
      _ apply: () throws(Failure) -> Result,
      onChange: @escaping @Sendable (borrowing ObservationTracking.Event) -> Void
    ) throws(Failure) -> Result

     

    연속 관찰 API

    public func withContinuousObservationTracking(
      options: ObservationTracking.Options,
      @_inheritActorContext apply: @isolated(any) @Sendable @escaping (borrowing ObservationTracking.Event) -> Void
    ) -> ObservationTracking.Token

     

    새로운 타입들은 ObservationTracking 네임스페이스에 들어갑니다.

    public struct ObservationTracking { }

     

     


    ObservationTracking.Options

    옵션은 3가지 종류가 있고, 조합해서 사용할 수 있습니다.

    extension ObservationTracking {
      public struct Options {
        public init()
    
        public static var willSet: Options { get }
        public static var didSet: Options { get }
        public static var `deinit`: Options { get }
      }
    }
    
    extension ObservationTracking.Options: SetAlgebra { }
    extension ObservationTracking.Options: Sendable { }
    • .willSet: 값이 설정되기 
    • .didSet: 값이 설정된 
    • .deinit: Observable 타입이 deinit될 때

    세 가지 모두 등록하면 프로퍼티 변경마다 2개 이벤트(willSet, didSet)가 발생하고, deinit 시 1개 이벤트가 발생합니다.

     

    OptionSet이 아니라 SetAlgebra를 채택한 이유는 내부 구현이 ABI stable하지 않을 수 있기 때문입니다.

     

     


    ObservationTracking.Event

    이벤트는 4가지 종류가 있습니다.

    extension ObservationTracking {
      public struct Event: ~Copyable {
        public struct Kind: Equatable, Sendable {
          public static var initial: Kind { get }
          public static var willSet: Kind { get }
          public static var didSet: Kind { get }
          public static var `deinit`: Kind { get }
        }
    
        public var kind: Kind { get }
    
        public func matches(_ keyPath: PartialKeyPath<some Observable>) -> Bool
        public func cancel()
      }
    }
    • .initial: 연속 이벤트 설정 시
    • .willSet: 프로퍼티 변경 전
    • .didSet: 프로퍼티 변경 후
    • .deinit: Observable 타입 해제 시

    KeyPath 매칭

    matches 메서드로 어떤 프로퍼티가 변경되었는지 확인할 수 있습니다.

    withObservationTracking(options: [.willSet]) {
      print(myObject.foo + myObject.bar)
    } onChange: { event in
      if event.matches(\MyObject.foo) {
        print("got a change of foo")
      }
      if event.matches(\MyObject.bar) {
        print("got a change of bar")
      }
    }
    
    myObject.bar += 1

    위 코드는 "got a change of bar"를 한 번 출력합니다.

     


    취소 기능

    cancel()로 관찰을 중단할 수 있습니다.

    예를 들어 .willSet 이벤트에서 취소하면, 해당하는 .didSet 이벤트는 발생하지 않습니다.

     


    Deinit 이벤트

    Observable 객체가 해제될 때도 이벤트를 받을 수 있습니다.

    var myObject: MyObject? = MyObject()
    
    withObservationTracking(options: [.deinit]) {
      if let myObject {
        print(myObject.foo + myObject.bar)
      }
    } onChange: { event in
      print("got a deinit event")
    }
    
    myObject = nil

     

    deinit 이벤트를 받았을 때 weak reference는 이미 nil이지만, 객체가 완전히 deinit되었는지는 보장하지 않습니다.

     

     


    예시

    _ = withObservationTracking(options: [.willSet, .didSet, .deinit]) {
      observable.property
    } onChange: { event in
      switch event.kind {
      case .initial: print("initial event")
      case .willSet: print("property will set")
      case .didSet: print("property did set")
      case .deinit: print("an Observable instance deallocated")
      }
    }
    
    observable.property += 1

     

    프로퍼티 변경 시 출력 시 이렇게 프린트 될거에요.

    property will set
    property did set
    
    • .willSet 이벤트에서는 값이 아직 저장되지 않음
    • .didSet 이벤트에서는 값이 이미 저장됨

    객체 해제 시 출력 시 이렇게 프린트 됩니다.

    an Observable instance deallocated

     

     


    연속 관찰 (Continuous Tracking)

    withContinuousObservationTracking은 이벤트 발생 후 자동으로 재관찰합니다.

    extension ObservationTracking {
      public struct Token: ~Copyable {
        public consuming func cancel()
      }
    }
    

    토큰을 유지하는 동안 계속 관찰하고, 토큰을 해제하거나 cancel()을 호출하면 중단됩니다.

     


    Actor Isolation 유지

    중요한 점은 클로저가 호출된 컨텍스트의 isolation을 유지한다는 겁니다.

    @MainActor
    final class Controller {
      var view: MyView
      var model: MyObservable
      let synchronization: ObservationTracking.Token
    
      init(view: MyView, model: MyObservable) {
        synchronization = withContinuousObservationTracking(options: [.willSet]) { [view, model] event in
          view.label.text = model.someStringValue
        }
      }
    }

    @MainActor에서 호출했으니, 클로저도 항상 main actor에서 실행됩니다.

     

    다음 suspension point에서 호출되므로 타이밍이 예측 가능합니다.

     

     


    호환성

    Source compatibility

    ObservationTracking 네임스페이스로 캡슐화되어 있어서 기존 코드와 충돌이 없습니다.

    새로운 메서드도 명확한 오버로드라서 문제없습니다.

     

    ABI compatibility

    ObservationTracking.Options의 내부 구조는 변경될 수 있어서 OptionSet 대신 SetAlgebra를 유지해야 합니다.

     


    실무에서의 영향

    이 제안의 가장 큰 의미는 기존 동기 시스템을 안전하게 마이그레이션할 수 있다는 겁니다.

    concurrency 이전의 프레임워크들은 값 변경에 대한 동기 콜백이 필요했는데, 이제 이런 시스템을 concurrency-safe한 환경으로 옮길 수 있습니다.

    보일러플레이트 코드도 크게 줄어들 거예요.

     


    향후 방향

    Swift 언어 레벨에서 새로운 property observer가 추가되면 (예: modified), ObservationTracking.Options에도 해당 옵션을 추가하는 걸 고려해야 합니다.

    @Observable 매크로가 그걸 채택하면 자연스럽게 Observation에도 반영되어야 하겠죠.

     


    고려된 대안들

    기본값으로 .willSet?

    withContinuousObservationTracking에 기본값으로 .willSet을 줄 수도 있었습니다.

    기존 withObservationTracking이 .willSet처럼 동작하니까요.

    하지만 클로저 시그니처가 다르기 때문에 명시적으로 옵션을 전달하도록 했습니다.

     

    기존 SPI를 그냥 API로?

    기존 SPI를 그대로 API로 승격시킬 수도 있었지만, 옵션 파라미터로 확장할 수 없다는 문제가 있었습니다.

    특히 deinit 같은 새로운 기능을 추가할 여지가 없었죠.

     

    옵션을 명시하는 게 더 명확한 디자인이라고 판단했습니다.

     

     


    Conclusion

    현재 SE-0506은 2026년 2월 3일까지 리뷰가 진행 중입니다.

    대부분의 경우 기존 @Observable로 충분하지만, 동기 시스템과 연동하거나 세밀한 제어가 필요한 케이스에서는 정말 유용할 것 같습니다.

    특히 레거시 UI 프레임워크를 modern Swift Concurrency로 마이그레이션하는 과정에서 큰 도움이 될 거예요.

    SwiftUI 내부에서도 이미 비슷한 메커니즘을 사용하고 있었다고 하니, 검증된 접근법이라고 볼 수 있겠네요 😃

     


    References

     

    swift-evolution/proposals/0506-advanced-observation-tracking.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

     

    [Pitch] Advanced Observation Tracking

    Introduction Observation has one primary public entry point for observing the changes to @Observable types. This proposal adds two new versions that allow more fine-grained control and advanced behaviors. In particular, it is not intended to be a natural p

    forums.swift.org

     

    SE-0506 Advanced Observation Tracking

    Hello, Swift community! The review of SE-0506: Advanced Observation Tracking begins now and runs through February 3, 2026. Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you wo

    forums.swift.org

Designed by Tistory.