7.1 KiB
7.1 KiB
id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
| id | title | category | status | source_trust_level | verification_status | created_at | updated_at | tags | tech_stack | applied_in | aliases | |||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ios-swift-macros-deep | Swift Macros — compile-time codegen | Coding | draft | B | conceptual | 2026-05-09 | 2026-05-09 |
|
|
|
Swift Macros
Swift 5.9+ 의 hygenic codegen. Freestanding (
#stringify(x)) + attached (@Observable). Boilerplate ↓.
📖 핵심 개념
- Compile-time expansion (no runtime).
- Hygenic (scope 격리).
- SwiftSyntax 가 backbone.
- Type-safe.
💻 코드 패턴
Freestanding macro
let (result, source) = #stringify(2 + 3)
print(result) // 5
print(source) // "(2 + 3)"
→ #stringify 가 expression + source string.
@Observable (Apple)
import Observation
@Observable
class UserModel {
var name: String = ""
var age: Int = 0
}
// Auto-generates:
// - Tracking infrastructure
// - withMutation calls
// - Observer registration
→ SwiftUI 가 자동 observe.
@Model (SwiftData)
import SwiftData
@Model
class Item {
var name: String
var quantity: Int
init(name: String, quantity: Int) {
self.name = name
self.quantity = quantity
}
}
// Auto-generates:
// - Persistence
// - Relationships
// - Identity
Custom macro 작성
// Package.swift
import CompilerPluginSupport
import PackageDescription
let package = Package(
name: "MyMacros",
platforms: [.macOS(.v13)],
products: [
.library(name: "MyMacros", targets: ["MyMacros"]),
],
dependencies: [
.package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"),
],
targets: [
.macro(
name: "MyMacroPlugin",
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
]
),
.target(name: "MyMacros", dependencies: ["MyMacroPlugin"]),
]
)
Stringify implementation
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.arguments.first?.expression else {
return ""
}
return "(\(arg), \(literal: arg.description))"
}
}
// MyMacros 모듈
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MyMacroPlugin", type: "StringifyMacro")
Attached macro
@AddInit
struct User {
let name: String
let age: Int
}
// Auto-generates:
// init(name: String, age: Int) {
// self.name = name
// self.age = age
// }
public struct AddInitMacro: MemberMacro {
public static func expansion(...) throws -> [DeclSyntax] {
// Generate init from struct properties.
}
}
@Test (Swift Testing)
import Testing
@Test
func addTwoNumbers() {
#expect(1 + 1 == 2)
}
@Test(arguments: [(1, 2, 3), (5, 5, 10)])
func add(_ a: Int, _ b: Int, _ expected: Int) {
#expect(a + b == expected)
}
→ XCTest 의 modern.
Use case
- @Observable: SwiftUI state.
- @Model: SwiftData persistence.
- @Test: testing.
- Custom: builder, Codable boilerplate, lens.
→ Boilerplate 가 큰 → macro 의 답.
Power 와 cost
+ Boilerplate 줄임.
+ Type-safe.
+ Compile-time error.
- Compile time ↑.
- Debug 어려움 (expanded code).
- Learning curve.
→ Library author 가 자주 작성.
App dev 가 사용 만 흔함.
Diagnostics
public static func expansion(...) throws -> [DeclSyntax] {
guard let structDecl = declaration.as(StructDeclSyntax.self) else {
throw MacroError.notStruct
}
if structDecl.members.isEmpty {
context.diagnose(Diagnostic(
node: declaration,
message: SimpleDiagnosticMessage(
message: "Struct has no members",
diagnosticID: .init(domain: "MyMacros", id: "empty"),
severity: .error
)
))
}
// ...
}
→ Compile error in Xcode.
Expanded code 보기
Xcode Editor → Right-click → Expand Macro.
또는:
swift -dump-ast file.swift
→ Generated code 검토.
Conformance macro
@AddCodable
struct User {
let name: String
}
// Generates: Codable conformance.
→ Boilerplate ↓ (Swift 가 자동 Codable 도 OK 가, custom logic 필요 시 macro).
Macro vs property wrapper
Property wrapper:
- Runtime.
- 작은 logic.
- 매 access 가 cost.
Macro:
- Compile-time.
- 큰 codegen.
- 0 runtime cost.
→ @Published (옛 Combine) → @Observable (modern macro).
Macro vs protocol extension
Protocol extension:
- 다이나믹 dispatch.
- 매 type 가 자체.
Macro:
- 정적 codegen.
- 정밀 control.
→ 매 use case 가 다름.
함정
- Compile time 폭발 (큰 macro).
- Diagnostics 가 confusing.
- Macro 의 변경 = 모든 user recompile.
- Test 가 어려움 (compile-time).
- IDE auto-complete 가 약함.
Production examples
- @Observable (Apple, Swift Observation framework).
- @Model (SwiftData).
- @Test (Swift Testing).
- @AsyncFailable (실험).
- TCA (The Composable Architecture)의 @Reducer.
Library author 의 use case
- Codable 보다 정밀 JSON.
- Protocol witness table.
- Dependency injection.
- DSL builder.
- Lens / optic.
Compile time
큰 macro = 큰 expansion = 큰 compile.
- 10 macro × 1000 line = 10000 line generated.
- Incremental compile 가 partial.
→ Profile + optimize.
Macro testing
import SwiftSyntaxMacrosTestSupport
func testStringify() {
assertMacroExpansion(
'''
#stringify(1 + 2)
''',
expandedSource: '''
(1 + 2, "1 + 2")
''',
macros: ['stringify': StringifyMacro.self]
)
}
→ Snapshot test.
vs Sourcery / GYB (옛)
Sourcery: 외부 tool, code generation.
GYB: Apple internal.
Swift Macros: native, type-safe.
→ Macro 가 modern.
Future
2026: Macros 가 mainstream.
- 더 많은 framework 가 macro.
- TCA / Vapor 가 macro 채택.
- Codegen ecosystem 발달.
🤔 의사결정 기준
| 작업 | 추천 |
|---|---|
| State management | @Observable |
| Persistence | @Model (SwiftData) |
| Testing | @Test |
| Boilerplate (init, codable) | Custom macro |
| 작은 codegen | Property wrapper |
| 외부 tool | Sourcery (legacy) |
❌ 안티패턴
- 모든 거 macro: compile time.
- Diagnostics 없음: bad UX.
- Test 없음: silent break.
- Macro 의 macro: complexity.
- Big logic in macro: compile slow.
🤖 LLM 활용 힌트
- Swift 5.9+ macro.
- Apple 의 @Observable / @Model / @Test.
- Library author 의 답.
- SwiftSyntax + SwiftCompilerPlugin.