
Swift 5.9 버전부터 적용된 컴파일 시간에 코드 생성, 검증을 가능하게 해주는 기능
텍스트를 추상 구문 트리 (Abstract Syntax Tree, AST) 인 구조화된 데이터로 변환해주는 라이브러리
그냥 텍스트: let a = 1 + 2
SwiftSyntax가 분석한 구조 (AST)
매크로는 이 AST를 직접 탐색하고 조작
AST를 보고 '이항 연산자 표현식' 노드를 통째로 문자열로 변환하여 "1 + 2"를 얻음.
단순 텍스트 검색/치환보다 훨씬 정교하고 안전하다는 이점.
매크로는 하나의 파일에 통째로 들어있지 않고, 두 부분으로 명확하게 나뉘어있음.
@freestanding(expression)
public macro stringify<T>(_ expression: T) -> (T, String) = #externalMacro(...)
이름: stringify
종류: @freestanding(expression) (표현식 매크로)
입력: T 타입의 표현식 하나
출력: (T, String) 타입의 튜플
실제 위치: #externalMacro(...) (실제 로직은 다른 곳에 있음)
컴파일러는 이 정보로 매크로의 사용법, 매크로의 결과 타입을 미리 인지 가능
public struct StringifyMacro: ExpressionMacro {
public static func expansion(...) -> ExprSyntax {
// ... SwiftSyntax로 코드를 분석하고 ...
// ... 새로운 코드 조각을 만들어서 반환 ...
}
}
컴파일러는 선언부를 확인하고 구현부를 찾아가 실행
매크로는 컴파일 과정에 직접 관여하는 강력한 기능이므로, 보안 문제 방지를 위해 샌드박스 격리 환경에서 실행
매크로의 구현부를 샌드박스에서 실행
할 수 없는 것 ❌:
파일 시스템 접근 (파일 읽기/쓰기)
네트워크 통신
컴파일 중인 다른 파일의 정보 접근
할 수 있는 것 ✅:
입력으로 받은 코드 조각(AST)을 분석하기
새로운 코드 조각(AST)을 만들어서 반환하기
매크로가 할 수 있는 일을 코드 변환으로 제한시켜 보안 문제 해결
코드 중복 감소: 반복적으로 작성해야 하는 상용구 코드를 매크로로 대체하여 코드의 양을 줄일 수 있음.
가독성 향상: 복잡한 로직을 매크로 뒤에 숨겨 코드의 의도를 명확하게 표현
안전성: 매크로는 컴파일 시점에 Swift 구문으로 확장되고 타입 검사를 거치므로, 텍스트 기반의 코드 생성 방식보다 안전합니다. Xcode에서 매크로가 확장된 코드를 미리 확인하는 기능도 제공하여 디버깅을 용이하게 함.
특정 선언에 붙지 않고 독립적으로 사용되는 매크로
새로운 값을 생성하거나 특정 조건을 확인하는데 사용
#기호로 시작하며 새로운 값을 생성
#URL: 문자열을 기반으로 컴파일 시점에 URL의 유효성을 검사하고 URL 객체를 생성
#stringify: 인자로 전달된 코드 표현식을 (결과값, 코드 문자열) 튜플로 변환
let a = 10
let b = 20
// #stringify 매크로에 덧셈 표현식을 전달
let (result, code) = #stringify(a + b)
print("The result of '\(code)' is \(result)")
// 컴파일 시 확장된 코드
// 위의 코드는 컴파일 시 아래처럼 변환
let (result, code) = (a + b, "a + b")
print("The result of '\(code)' is \(result)")
#기호로 시작하며 새로운 코드를 생성하는 매크로
변수, 함수, 클래스 등도 가능
비슷한 패턴의 함수들을 수십 개 만들어야 할 때, 코드 중복 없이 자동화 가능
#createUnitFunctions(units: ["Meters": 1.0, "Kilometers": 1000.0, "Miles": 1609.34])
// 이제 매크로가 생성해준 함수들을 그냥 가져다 쓰기
logInMeters(value: 100)
logInKilometers(value: 5000)
logInMiles(value: 10000)
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
public struct CreateUnitFunctionsMacro: DeclarationMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
// 1. 매크로 인자로 전달된 딕셔너리 구문 가져오기
guard let dictionary = node.argumentList.first?.expression.as(DictionaryExprSyntax.self) else {
fatalError("컴파일러 에러: 딕셔너리 인자가 필요합니다.")
}
// 2. 딕셔너리의 각 요소를 순회하며 [DeclSyntax] 배열을 생성
let functionDeclarations = dictionary.elements.compactMap { element -> DeclSyntax? in
// 3. 딕셔너리의 키(String)와 값(Double)을 추출
guard let unitName = element.keyExpression.as(StringLiteralExprSyntax.self)?.segments.first?.content.text,
let unitValue = element.valueExpression.as(FloatLiteralExprSyntax.self)?.literal.text ?? element.valueExpression.as(IntegerLiteralExprSyntax.self)?.literal.text
else {
return nil
}
// 4. 추출한 정보를 바탕으로 함수 이름과 단위 심볼을 생성
let functionName = "logIn" + unitName
let unitSymbol = getUnitSymbol(from: unitName)
// 5. 생성할 함수 코드를 문자열로 조립
let functionCode = """
func \(raw: functionName)(value: Double) {
print("\\(value / \(raw: unitValue))\(raw: unitSymbol)")
}
"""
// 6. 조립된 코드 문자열을 DeclSyntax로 변환
return DeclSyntax(stringLiteral: functionCode)
}
// 7. 생성된 모든 함수 선언 배열을 반환
return functionDeclarations
}
// 단위 이름을 기반으로 짧은 심볼을 반환하는 헬퍼 함수
private static func getUnitSymbol(from unitName: String) -> String {
switch unitName.lowercased() {
case "meters": return "m"
case "kilometers": return "km"
case "miles": return "mi"
default: return unitName
}
}
}
클래스, 구조체, 함수 등 특정 선언에 @ 기호를 사용하여 첨부되는 매크로
첨부된 대상에 코드를 추가하거나 수정하는 역할
해당 타입에 새로운 멤버(프로퍼티, 메서드 등)을 추가
import SwiftUI
import Combine // Published를 사용하기 위해 Combine을 import
// 1. ObservableObject 프로토콜 채택
class CounterViewModel_Old: ObservableObject {
// 2. 변경을 감지할 모든 프로퍼티에 @Published 붙이기
@Published var count = 0
@Published var anotherNumber = 100 // 이 프로퍼티도 변경되면 뷰가 업데이트
func increment() {
count += 1
}
}
struct ContentView_Old: View {
// 3. @StateObject 또는 @ObservedObject로 ViewModel을 구독
@StateObject private var viewModel = CounterViewModel_Old()
var body: some View {
VStack(spacing: 20) {
Text("Count: \(viewModel.count)")
.font(.largeTitle)
Button("Increment") {
viewModel.increment()
}
}
}
}
--------------------------------------------------
import SwiftUI
// 1. 클래스 위에 @Observable 매크로 한 줄 추가
@Observable
class CounterViewModel {
// @Published 필요 X 프로퍼티를 그냥 선언하면 끝.
var count = 0
var anotherNumber = 100
func increment() {
count += 1
}
}
struct ContentView: View {
// 2. @StateObject 대신 @State로 간단하게 선언
@State private var viewModel = CounterViewModel()
var body: some View {
VStack(spacing: 20) {
Text("Count: \(viewModel.count)")
.font(.largeTitle)
Button("Increment") {
viewModel.increment()
}
}
}
}
ObservableObject와 @Observable의 차이
저장 프로퍼티(stored property)에 붙어서 getter와 setter를 추가하거나 수정
struct Player {
// 체력은 항상 0에서 100 사이여야 함
@Clamped(range: 0...100)
var health: Int = 100
}
var player = Player()
player.health = 150
print(player.health) // 100
player.health = -50
print(player.health) // 0
-----------------------------------------------------
struct Player {
private var _health: Int = 100 // 매크로가 생성한 비공개 저장소
var health: Int {
// ▼▼▼ 매크로가 생성한 접근자(accessor) ▼▼▼
get { _health }
set {
_health = max(0, min(100, newValue))
}
// ▲▲▲ 매크로가 생성한 접근자(accessor) ▲▲▲
}
}
타입에 붙어 내부의 모든 멤버에게 특정 속성을 일괄적으로 추가
@Published를 붙여주는 매크로 (직접 만든 매크로)import Combine
@AllPublished
class UserProfileViewModel {
var username: String = "Guest"
var score: Int = 0
// isPremium은 Published 되면 안 됨
@NotPublished var isPremium: Bool = false
}
------------------------------------------------------
import Combine
class UserProfileViewModel {
// ▼▼▼ 매크로가 속성을 추가함 ▼▼▼
@Published var username: String = "Guest"
@Published var score: Int = 0
// ▲▲▲ 매크로가 속성을 추가함 ▲▲▲
var isPremium: Bool = false // 이 속성은 @NotPublished 때문에 제외됨
}
함수나 프로퍼티에 붙어서 그 선언과 동일한 레벨(동료, peer)에 새로운 선언을 추가
completion handler 기반의 비동기 함수의 async/await 버전을 자동으로 생성해주는 매크로 (직접 만든 매크로)@AddAsync
func fetchUserData(id: String, completion: @escaping (Result<String, Error>) -> Void) {
// ...네트워크 통신 후 completion 호출...
completion(.success("사용자 데이터"))
}
-------------------------------------------------
// ▼▼▼ 매크로가 생성한 '동료(peer)' 함수 ▼▼▼
func fetchUserData(id: String) async throws -> String {
try await withCheckedThrowingContinuation { continuation in
fetchUserData(id: id) { result in
continuation.resume(with: result)
}
}
}
// ▲▲▲ 매크로가 생성한 '동료(peer)' 함수 ▲▲▲
// async/await 버전 함수 사용 가능
Task {
if let data = try? await fetchUserData(id: "123") {
print(data) // "사용자 데이터"
}
}
타입에 붙어서 extension 블록 전체를 추가
주로 프로토콜 채택 및 관련 구현을 자동화하는 데 사용
Equatable 프로토콜을 자동으로 준수하게 만드는 매크로 (직접 만든 매크로)@AutoEquatable
struct User {
let id: UUID
let name: String
var score: Int
}
let user1 = User(id: UUID(), name: "Steve", score: 100)
let user2 = User(id: user1.id, name: "Steve", score: 100)
print(user1 == user2) // true
-------------------------------------------------
struct User {
let id: UUID
let name: String
var score: Int
}
// ▼▼▼ 매크로가 생성한 extension 블록 ▼▼▼
extension User: Equatable {
static func == (lhs: User, rhs: User) -> Bool {
lhs.id == rhs.id &&
lhs.name == rhs.name &&
lhs.score == rhs.score
}
}
// ▲▲▲ 매크로가 생성한 extension 블록 ▲▲▲
빌드 시간 증가: 매크로는 컴파일 시 코드를 생성하는 추가적인 단계를 거치므로 빌드 시간이 늘어 날 수 있음.
복잡성: 매크로를 사용하는 것은 쉽지만, 직접 만드는 것은 SwiftSyntax에 대한 이해가 필요
참고
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/macros/
https://github.com/swiftlang/swift-evolution/blob/main/proposals/0382-expression-macros.md