Files
2nd/10_Wiki/Topics/Coding/iOS_Swift_Macros.md
T
2026-05-09 21:08:02 +09:00

6.7 KiB


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)

// @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] = []
    
    // ...
}
// SwiftData
@Model
final class Note {
    var title: String
    var body: String
    var createdAt: Date
    
    init(title: String, body: String) { ... }
}
// SwiftUI Preview
#Preview("Light") {
    ContentView()
}

#Preview("Dark") {
    ContentView()
        .preferredColorScheme(.dark)
}
// 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/
// 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

// MyMacros.swift (declaration)
@freestanding(expression)
public macro stringify<T>(_ 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 추가)

@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

// MyMacrosPlugin.swift
import SwiftCompilerPlugin
import SwiftSyntaxMacros

@main
struct MyMacrosPlugin: CompilerPlugin {
    let providingMacros: [Macro.Type] = [
        StringifyMacro.self,
        CaseDetectionMacro.self,
    ]
}

Test

import SwiftSyntaxMacrosTestSupport
import XCTest

final class StringifyMacroTests: XCTestCase {
    func testStringify() {
        assertMacroExpansion(
            "#stringify(2 + 3)",
            expandedSource: """
            ("2 + 3", 2 + 3)
            """,
            macros: ["stringify": StringifyMacro.self]
        )
    }
}

Diagnostic (에러)

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 자동 생성

@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<User, Error> = .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 항상.

🔗 관련 문서