Files
2nd/10_Wiki/Topics/Coding/iOS_Swift_Macros_Deep.md
T
2026-05-10 22:08:15 +09:00

7.1 KiB
Raw Blame History

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
ios
swift
vibe-coding
language applicable_to
Swift
iOS
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

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.

🔗 관련 문서