[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,362 @@
|
||||
---
|
||||
id: ios-swift-macros-deep
|
||||
title: Swift Macros — compile-time codegen
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [ios, swift, vibe-coding]
|
||||
tech_stack: { language: "Swift", applicable_to: ["iOS"] }
|
||||
applied_in: []
|
||||
aliases: [Swift Macros, freestanding macro, attached macro, SwiftSyntax, codegen]
|
||||
---
|
||||
|
||||
# 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
|
||||
```swift
|
||||
let (result, source) = #stringify(2 + 3)
|
||||
print(result) // 5
|
||||
print(source) // "(2 + 3)"
|
||||
```
|
||||
|
||||
→ `#stringify` 가 expression + source string.
|
||||
|
||||
### @Observable (Apple)
|
||||
```swift
|
||||
import Observation
|
||||
|
||||
@Observable
|
||||
class UserModel {
|
||||
var name: String = ""
|
||||
var age: Int = 0
|
||||
}
|
||||
|
||||
// Auto-generates:
|
||||
// - Tracking infrastructure
|
||||
// - withMutation calls
|
||||
// - Observer registration
|
||||
```
|
||||
|
||||
→ SwiftUI 가 자동 observe.
|
||||
|
||||
### @Model (SwiftData)
|
||||
```swift
|
||||
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 작성
|
||||
```swift
|
||||
// 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
|
||||
```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.arguments.first?.expression else {
|
||||
return ""
|
||||
}
|
||||
return "(\(arg), \(literal: arg.description))"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```swift
|
||||
// MyMacros 모듈
|
||||
@freestanding(expression)
|
||||
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MyMacroPlugin", type: "StringifyMacro")
|
||||
```
|
||||
|
||||
### Attached macro
|
||||
```swift
|
||||
@AddInit
|
||||
struct User {
|
||||
let name: String
|
||||
let age: Int
|
||||
}
|
||||
|
||||
// Auto-generates:
|
||||
// init(name: String, age: Int) {
|
||||
// self.name = name
|
||||
// self.age = age
|
||||
// }
|
||||
```
|
||||
|
||||
```swift
|
||||
public struct AddInitMacro: MemberMacro {
|
||||
public static func expansion(...) throws -> [DeclSyntax] {
|
||||
// Generate init from struct properties.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### @Test (Swift Testing)
|
||||
```swift
|
||||
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
|
||||
```swift
|
||||
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
|
||||
```swift
|
||||
@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
|
||||
```swift
|
||||
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.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[iOS_Swift_Macros]]
|
||||
- [[iOS_SwiftData_Patterns]]
|
||||
- [[iOS_Swift_Concurrency_async_await]]
|
||||
Reference in New Issue
Block a user