--- id: ios-swift-macros title: Swift Macros — Compile-time Code Generation category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [ios, swift, macro, vibe-coding] tech_stack: { language: "Swift 5.9+", applicable_to: ["iOS"] } applied_in: [] aliases: [Swift Macros, @attached, @freestanding, SwiftSyntax, Codable derivation] --- # Swift Macros (Swift 5.9+) > Compile-time 코드 생성. **Boilerplate 제거 (Codable, Observation, Mocking)**. SwiftSyntax 로 작성. 옛 propertyWrapper / @objc dynamic 보다 강력. ## 📖 핵심 개념 - @freestanding: `#myMacro(...)` — expression / declaration 자체. - @attached: `@MyMacro` — 다른 declaration 에 붙음. - 종류: member, peer, accessor, conformance, extension. - SwiftSyntax: AST 변형. ## 💻 코드 패턴 ### 사용 (built-in) ```swift // @Observable (iOS 17+, Observation framework) @Observable class ViewModel { var name = "" var items: [Item] = [] } // 매크로 expand: class ViewModel { private let _$observationRegistrar = ObservationRegistrar() @ObservationTracked var name = "" @ObservationTracked var items: [Item] = [] // ... } ``` ```swift // SwiftData @Model final class Note { var title: String var body: String var createdAt: Date init(title: String, body: String) { ... } } ``` ```swift // SwiftUI Preview #Preview("Light") { ContentView() } #Preview("Dark") { ContentView() .preferredColorScheme(.dark) } ``` ```swift // Stringify (test) let result = #stringify(1 + 2) // → ("1 + 2", 3) ``` ### 자체 macro 작성 — package 구조 ``` MyMacros/ ├── Package.swift ├── Sources/ │ ├── MyMacros/ (consumer 가 import) │ │ └── MyMacros.swift (declarations) │ └── MyMacrosImpl/ (compiler plugin) │ ├── MyMacrosPlugin.swift │ └── ConstantMacro.swift └── Tests/ ``` ```swift // Package.swift import PackageDescription import CompilerPluginSupport let package = Package( name: "MyMacros", platforms: [.iOS(.v17), .macOS(.v14)], products: [.library(name: "MyMacros", targets: ["MyMacros"])], dependencies: [ .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"), ], targets: [ .macro(name: "MyMacrosImpl", dependencies: [ .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), ]), .target(name: "MyMacros", dependencies: ["MyMacrosImpl"]), ] ) ``` ### Freestanding expression macro ```swift // MyMacros.swift (declaration) @freestanding(expression) public macro stringify(_ value: T) -> (String, T) = #externalMacro( module: "MyMacrosImpl", type: "StringifyMacro" ) // MyMacrosImpl/StringifyMacro.swift import SwiftSyntax import SwiftSyntaxMacros public struct StringifyMacro: ExpressionMacro { public static func expansion( of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext ) -> ExprSyntax { guard let arg = node.argumentList.first?.expression else { fatalError("compiler bug") } return "(\(literal: arg.description), \(arg))" } } // 사용 let r = #stringify(2 + 3) // ("2 + 3", 5) ``` ### Attached member macro (struct 에 method 추가) ```swift @attached(member, names: arbitrary) public macro CaseDetection() = #externalMacro(...) @CaseDetection enum Status { case open case paid case shipped } // Expand 후: extension Status { var isOpen: Bool { if case .open = self { return true } else { return false } } var isPaid: Bool { ... } var isShipped: Bool { ... } } ``` ### Plugin ```swift // MyMacrosPlugin.swift import SwiftCompilerPlugin import SwiftSyntaxMacros @main struct MyMacrosPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ StringifyMacro.self, CaseDetectionMacro.self, ] } ``` ### Test ```swift import SwiftSyntaxMacrosTestSupport import XCTest final class StringifyMacroTests: XCTestCase { func testStringify() { assertMacroExpansion( "#stringify(2 + 3)", expandedSource: """ ("2 + 3", 2 + 3) """, macros: ["stringify": StringifyMacro.self] ) } } ``` ### Diagnostic (에러) ```swift public struct MyMacro: MemberMacro { public static func expansion( of node: AttributeSyntax, providingMembersOf decl: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { guard decl.is(StructDeclSyntax.self) else { context.diagnose(Diagnostic( node: node, message: SimpleDiagnosticMessage( message: "@MyMacro can only be applied to structs", diagnosticID: MessageID(domain: "MyMacros", id: "structOnly"), severity: .error ) )) return [] } // ... } } ``` ### 실용 예 — Mock 자동 생성 ```swift @AutoMock protocol UserService { func fetchUser(id: String) async throws -> User func saveUser(_ user: User) async throws } // Expand 후 class UserServiceMock: UserService { var fetchUserCalls: [String] = [] var fetchUserResult: Result = .failure(MockError.notSet) func fetchUser(id: String) async throws -> User { fetchUserCalls.append(id) return try fetchUserResult.get() } var saveUserCalls: [User] = [] func saveUser(_ user: User) async throws { saveUserCalls.append(user) } } ``` ## 🤔 의사결정 기준 | 사용 | 추천 | |---|---| | Boilerplate 제거 | Macro | | Codable customize | Macro 또는 ExtendingMacro | | Mock 생성 | AutoMock-style macro | | 단순 helper | 일반 함수 / extension 충분 | | Ad-hoc | macro 만들지 말 것 | | Public library | macro 가 powerful | ## ❌ 안티패턴 - **모든 거 macro**: 디버깅 어려움. 일반 코드 우선. - **Macro 안 외부 의존 (network)**: deterministic 깨짐. 순수 syntax 변환만. - **Diagnostic 없음**: 사용자 잘못 사용 시 cryptic error. - **Test 없음**: regression 잡기 어려움. - **너무 복잡 generated code**: 디버그 못 함. 단순 전개. - **String concat 으로 syntax**: SwiftSyntax 안전. - **Macro 가 다른 macro 의존**: order 의존. ## 🤖 LLM 활용 힌트 - @Observable / @Model / SwiftData / Preview = built-in. - 자체 macro = SwiftSyntax 학습 필요. - Diagnostic + test 항상. ## 🔗 관련 문서 - [[iOS_SwiftUI_State_Property_Wrappers]] - [[iOS_SwiftData_Patterns]] - [[iOS_Swift_Concurrency_Actor_Patterns]]