6.7 KiB
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 항상.