-
Swift Macro - attachIOS/WWDC24 2025. 6. 8. 17:16
여태까지 #을 사용하는 freestanding 메크로를 사용했음. 메크로의 종류가 2가지인 것을 다시 표로 상기시켜 보고 이번엔 attach 메크로에 알아보겠습니다.
*사전지식
두가지 메크로가 같은 코드에 적용되면 어떤 게 먼저 적용될지에 대한 주제로 고민한다면 정답은 어느 쪽도 상관이 없음 각 메크로마다 역할이 중복되지 않아서 어떤 순서로 선언하거나 실행해도 결과는 같음

attach 메크로 종류

attached peer
기존 함수, 타입, 변수 등에 추가적인 선언을 생성해주는 메크로
예시로, 비동기 처리에서 async와 completion handler를 통합할 때 보일러 플레이트 코드를 제거하는 예제로 쓰일 수 있음
예시로, 비동기 처리에서 async와 completion handler를 통합할 때 보일러 플레이트 코드를 제거하는 예제로 쓰일 수 있음
func fetchAvatar(_ username: String) async -> Image? { ... } // 아래와 같이 만들기 위해선 async키워드, 완료 핸들러를 파라미터로 추가해야함. func fetchAvatar(_ username: String, onCompletion: @escaping (Image?) -> Void) { Task.detached { onCompletion(await fetchAvatar(username)) } }이럴 경우 수작업으로 작성해야 하는데 이 부분에서 peer 메크로가 처리할 수 있음.
이해가 좀 힘들 수 있지만 그 부분은 구현부가 생략되어서 그럼. 구현부에서 새로운 메서드 시그니처를 만들어주는 듯.
/// Overload an `async` function to add a variant that takes a completion handler closure as /// a parameter. @attached(peer, name: overloaded) macro AddCompletionHandler(parameterName: String = "completionHandler") /// Fetch the avatar for the user with `username`. @AddCompletionHandler(parameterName: "onCompletion") func fetchAvatar(_ username: String) async -> Image? { ... } // Begin expansion for "@AddCompletionHandler" /// Fetch the avatar for the user with `username`. /// Equivalent to ``fetchAvatar(username:)`` with /// a completion handler. func fetchAvatar( _ username: String, onCompletion: @escaping (Image?) -> Void ) { Task.detached { onCompletion(await fetchAvatar(username)) } } // End expansion for "@AddCompletionHandler"attached accessor
Swift 속성(var)의 get, set, willSet, didSet 같은 접근자(accessor)를 자동 생성하는 메크로
// Before: 반복되는 프로퍼티 연산 셋팅에서 보일러플레이트 코드 생성 struct Person: DictionaryRepresentable { init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] var name: String { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } var height: Measurement<UnitLength> { get { dictionary["height"]! as! Measurement<UnitLength> } set { dictionary["height"] = newValue } } var birthDate: Date? { get { dictionary["birth_date"] as! Date? } set { dictionary["birth_date"] = newValue as Any? } } } /// Adds accessors to get and set the value of the specified property in a dictionary /// property called `storage`. @attached(accessor) macro DictionaryStorage(key: String? = nil) struct Person: DictionaryRepresentable { init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] @DictionaryStorage var name: String // Begin expansion for "@DictionaryStorage" { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } // End expansion for "@DictionaryStorage" @DictionaryStorage var height: Measurement<UnitLength> // Begin expansion for "@DictionaryStorage" { get { dictionary["height"]! as! Measurement<UnitLength> } set { dictionary["height"] = newValue } } // End expansion for "@DictionaryStorage" @DictionaryStorage(key: "birth_date") var birthDate: Date? // Begin expansion for "@DictionaryStorage" { get { dictionary["birth_date"] as! Date? } set { dictionary["birth_date"] = newValue as Any? } } // End expansion for "@DictionaryStorage" }attached memberAttribute
이전에 언급했던 accessor Macro의 예제로 이어가겠음.
프로퍼티에 자동으로 연산프로퍼티를 적용했었음, 대신 각 프로퍼티에 attribute 호출자인 @DictionaryStorage를 붙여줘야 하는 반복작업이 존재했음.
/// Adds accessors to get and set the value of the specified property in a dictionary /// property called `storage`. @attached(accessor) macro DictionaryStorage(key: String? = nil) struct Person: DictionaryRepresentable { init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] @DictionaryStorage var name: String // Begin expansion for "@DictionaryStorage" { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } // End expansion for "@DictionaryStorage" @DictionaryStorage var height: Measurement<UnitLength> // Begin expansion for "@DictionaryStorage" { get { dictionary["height"]! as! Measurement<UnitLength> } set { dictionary["height"] = newValue } } // End expansion for "@DictionaryStorage" @DictionaryStorage(key: "birth_date") var birthDate: Date? // Begin expansion for "@DictionaryStorage" { get { dictionary["birth_date"] as! Date? } set { dictionary["birth_date"] = newValue as Any? } } // End expansion for "@DictionaryStorage" }이걸 해결하기 위해 memberAttribute를 사용해 멤버들에게 공통된 attribute를 부여할 수 있음.
/// Adds accessors to get and set the value of the specified property in a dictionary /// property called `storage`. @attached(memberAttribute) @attached(accessor) macro DictionaryStorage(key: String? = nil)하지만 특정 프로퍼티만 별도의 key를 명시해야 하는 경우 공통 커스텀 로직이 구현한 예외기 때문에 타입 전체에 붙은 attribute만으로 안되고 아래 코드와 같이 해결가능함
@DictionaryStorage struct Person: DictionaryRepresentable { init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] // Begin expansion for "@DictionaryStorage" @DictionaryStorage // End expansion for "@DictionaryStorage" var name: String // Begin expansion for "@DictionaryStorage" @DictionaryStorage // End expansion for "@DictionaryStorage" var height: Measurement<UnitLength> // 커스텀이 필요한 경우 @DictionaryStorage(key: "birth_date") var birthDate: Date? }attached memeber
이니셜라이저 자동 생성 init(dictionary:), init(rawValue:)
computed property 삽입 var description, var localizedName 변환 함수 자동 생성 toJSON(), toDTO(), enum 변환 switch @attached(member, names: named(<멤버 이름>), named(<함수 이름>), ...) // names들은 메크로가 컴파일러에게 <멤버이름>멤버들을 만들 예정이야 라는 의미예시상황: 특정 구조체의 멤버 변수나 이니셜라이저를 생성을 예로 들어보면 다음과 같은 결과로 코드를 줄일 수 있음
///before @DictionaryStorage struct Person: DictionaryRepresentable { init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] // Begin expansion for "@DictionaryStorage" @DictionaryStorage // End expansion for "@DictionaryStorage" var name: String // Begin expansion for "@DictionaryStorage" @DictionaryStorage // End expansion for "@DictionaryStorage" var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? } /// after -> member macro 적용후 @DictionaryStorage struct Person: DictionaryRepresentable { var name: String var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? }attached conformance
타입 선업에 특정 프로토콜 준수를 추가해 주는 메커니즘 예
Codable, Equatable 등. 메크로가 해당 프로토콜을 컴파일 타임에 자동으로 추가하도록 설정
@attached(conformance) public macro AutoCodable() = #externalMacro( module: "MyMacroPlugin", type: "AutoCodableMacro" ) public struct AutoCodableMacro: ConformanceMacro { public static func expansion( of node: AttributeSyntax, attachedTo declaration: some DeclGroupSyntax, providingConformances conformances: inout [TypeSyntax], in context: MacroExpansionContext ) throws { conformances.append(TypeSyntax(stringLiteral: "Codable")) } } @AutoCodable struct Book { let title: String let pages: Int } //after: 컴파일 시점 struct Book: Codable { let title: String let pages: Int }'IOS > WWDC24' 카테고리의 다른 글
Swift Macro - implement (0) 2025.06.01 Swift Macro - Intro (0) 2025.05.10 [WWDC24] Swift의 성능 살펴보기 (3/3) (0) 2025.02.09 [WWDC24] Swift의 성능 살펴보기 (2 / 3) (0) 2025.02.08 [WWDC24] Swift의 성능 살펴보기 (1 / 3) (0) 2024.12.07