티스토리 뷰
SE-0382, SE-0389, SE-0397이 결합되어 컴파일 시 구문을 변환하는 코드를 생성할 수 있는 '매크로' 기능
C++에서 매크로는 코드를 전처리하는 방법으로, 메인 컴파일러가 코드를 보기 전에 코드의 텍스트 교체를 효과적으로 수행하여 손으로 작성하고 싶지 않은 코드를 생성할 수 있다.
Swift의 매크로는 이와 비슷하지만 훨씬 더 강력하며, 따라서 훨씬 더 복잡하다. 또한 매크로를 사용하면 프로젝트의 Swift 코드가 컴파일되기 전에 동적으로 조작할 수 있으므로 컴파일 시점에 추가 기능을 삽입할 수 있다.
핵심 사항:
- 매크로는 단순한 문자열 대체가 아닌 유형 안전성이 있으므로 매크로가 작동할 데이터를 정확히 알려주어야 한다.
- 매크로는 빌드 단계에서 외부 프로그램으로 실행되며 기본 앱 타겟에 존재하지 않는다.
- 매크로는 단일 표현식을 생성하는
ExpressionMacro
, 게터와 세터를 추가하는AccessorMacro
, 타입을 프로토콜에 맞게 만드는ConformanceMacro
와 같이 여러 가지 작은 유형으로 나뉜다. - 매크로는 파싱된 소스 코드와 함께 작동하며, 조작 중인 프로퍼티의 이름이나 유형, 구조체 내부의 다양한 프로퍼티 등 코드의 개별 부분을 쿼리할 수 있다.
- 매크로는 샌드박스 내에서 작동하며 지정된 날짜에만 작동해야 한다.
특히 마지막 부분이 중요한데, Swift의 매크로 지원은 소스 코드를 이해하고 조작하기 위해 Apple의 SwiftSyntax 라이브러리를 기반으로 한다. 이 라이브러리를 매크로의 종속성으로 추가해야 한다.
간단한 매크로부터 시작하여 매크로가 어떻게 작동하는지 살펴보자. 매크로는 컴파일 시 실행되므로 앱이 빌드된 날짜와 시간을 반환하는 작은 매크로를 만들 수 있으며, 이는 디버그 진단에 유용하게 사용할 수 있다. 이 작업에는 여러 단계가 필요하며, 그 중 몇 단계는 기본 대상과 별도의 모듈에서 수행해야 한다.
먼저 매크로 확장을 수행하는 코드, 즉 #buildDate
를 2023-06-05T18:00:00Z와 같은 값으로 변환하는 코드를 만들어야 한다:
public struct BuildDateMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) -> ExprSyntax {
let date = ISO8601DateFormatter().string(from: .now)
return "\"\(raw: date)\""
}
}
중요: 이 코드는 메인 앱 타깃에 있으면 안 된다. 완성된 앱에 컴파일되는 것이 아니라 완성된 날짜 문자열만 넣어야 하기 때문이다.
동일한 모듈 내에서 CompilerPlugin
프로토콜을 준수하는 구조체를 생성하여 매크로를 내보낸다:
import SwiftCompilerPlugin
import SwiftSyntaxMacros
@main
struct MyMacrosPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
BuildDateMacro.self
]
}
그 다음 Package.swift의 대상 목록에 추가한다:
.macro(
name: "MyMacrosPlugin",
dependencies: [
.product(name: "SwiftSyntax", package: "swift-syntax"),
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
]
),
이것으로 외부 모듈에서 매크로를 생성하는 것이 끝났다.
나머지 코드는 메인 앱 타겟과 같이 매크로를 사용하고자 하는 모든 곳에서 수행된다.
이 작업은 매크로가 무엇인지 정의하는 것부터 시작하여 두 단계로 진행된다. 이 경우 매크로는 문자열을 반환하는 독립형 표현식 매크로로, MyMacrosPlugin
모듈 안에 존재하며 BuildDateMacro
라는 엄격한 이름을 가진다. 따라서 이 정의를 메인 타겟에 추가한다:
@freestanding(expression)
macro buildDate() -> String =
#externalMacro(module: "MyMacrosPlugin", type: "BuildDateMacro")
두 번째 단계는 다음과 같이 실제로 매크로를 사용하는 것이다:
print(#buildDate)
이 코드를 읽을 때 가장 중요한 점은 주요 매크로 기능, 즉 BuildDateMacro
구조체 내의 모든 코드가 빌드 시점에 실행되고 그 결과가 호출 부위에 다시 주입된다는 점이다. 따라서 위의 작은 print()
호출은 다음과 같이 재작성된다:
print("2023-06-05T18:00:00Z")
즉, 매크로 내부의 코드는 원하는 만큼 복잡할 수 있다.
완성된 코드는 실제로 우리가 반환한 문자열만 보기 때문에 우리가 원하는 방식으로 날짜를 만들 수 있다.
실제로 Swift 팀에서는 이러한 종류의 매크로를 사용하지 않는 것을 권장하는데, 그 이유는 일관된 출력으로 빌드하기를 원하기 때문이다. 동일한 출력이 주어지면 동일한 출력을 생성하는 매크로를 사용하면 증분 빌드와 같은 기능이 효율적으로 작동할 수 있기 때문이다.
좀 더 유용한 매크로를 만들어 보겠다. 이번에는 member attribute 매크로를 만들어 보겠다. 클래스와 같은 유형에 적용하면 클래스의 모든 멤버에 어트리뷰트를 적용할 수 있다. 이는 타입의 각 프로퍼티에 @objc
를 추가하는 이전의 @objcMembers
어트리뷰트와 개념이 동일하다.
예를 들어, 모든 프로퍼티에 @Published
를 사용하는 관찰 가능한 객체가 있는 경우 이 작업을 수행하는 간단한 @AllPublished
매크로를 작성할 수 있다. 먼저 매크로 자체를 작성한다:
public struct AllPublishedMacro: MemberAttributeMacro {
public static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingAttributesFor member: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AttributeSyntax] {
[AttributeSyntax(attributeName: SimpleTypeIdentifierSyntax(name: .identifier("Published")))]
}
}
둘째, 제공된 매크로 목록에 해당 매크로를 포함하자:
struct MyMacrosPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
BuildDateMacro.self,
AllPublishedMacro.self,
]
}
셋째, 메인 앱 타겟에서 매크로를 선언하고 이번에는 첨부된 멤버 속성 매크로로 표시한다:
@attached(memberAttribute)
macro AllPublished() = #externalMacro(module: "MyMacrosPlugin", type: "AllPublishedMacro")
이제 이를 사용하여 관찰 가능한 객체 클래스에 주석을 달 수 있다:
@AllPublished class User: ObservableObject {
var username = "Taylor"
var age = 26
}
매크로는 매개변수를 받아 동작을 제어할 수 있지만, 여기서는 복잡성이 실제로 급증하기 쉽다.
예를 들어, Swift 팀의 Doug Gregor는 예제 매크로의 작은 GitHub 저장소를 관리하고 있는데, 이 저장소에는 빌드 시점에 하드코딩된 URL이 유효한지 검사하는 깔끔한 매크로가 포함되어 있어 URL을 잘못 입력하면 빌드가 진행되지 않기 때문에 불가능한다.
앱 타겟에서 매크로를 선언하는 방법은 문자열 매개변수를 추가하는 등 간단하다:
@freestanding(expression) public macro URL(_ stringLiteral: String) -> URL = #externalMacro(module: "MyMacrosPlugin", type: "URLMacro")
사용 방법도 간단하다:
let url = #URL("https://swift.org")
print(url.absoluteString)
이렇게 하면 컴파일 시점에 URL이 올바른지 확인하기 때문에 url
이 선택적 인스턴스가 아닌 완전한 URL
인스턴스가 된다.
더 어려운 것은 실제 매크로 자체로, 전달된 'https://swift.org' 문자열을 읽고 이를 URL로 변환해야 한다. Doug의 버전이 더 철저하지만, 최소한으로 요약하면 다음과 같다:
public struct URLMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) -> ExprSyntax {
guard let argument = node.argumentList.first?.expression,
let segments = argument.as(StringLiteralExprSyntax.self)?.segments
else {
fatalError("#URL requires a static string literal")
}
guard let _ = URL(string: segments.description) else {
fatalError("Malformed url: \(argument)")
}
return "URL(string: \(argument))!"
}
}
SwiftSyntax는 놀랍지만, 제가 '발견할 수 있는' 기능은 아니다. 계속 진행하기 전에 세 가지를 더 추가하고 싶다.
첫째, 우리에게 주어진 MacroExpansionContext
값에는 현재 컨텍스트의 다른 이름과 충돌하지 않는 새 변수 이름을 생성하는 매우 유용한 makeUniqueName()
메서드가 있다. 완성된 코드에 새 이름을 삽입하려는 경우 makeUniqueName()
을 사용하는 것이 현명하다.
둘째, 매크로의 우려 사항 중 하나는 문제가 발생했을 때 코드를 디버깅하는 기능이다. 실제로 코드를 쉽게 살펴볼 수 없는 경우 무슨 일이 일어나고 있는지 추적하기가 어렵다. 리팩터링 작업으로 매크로를 확장하기 위해 이미 SourceKit 내부에서 일부 작업이 진행되었지만, 실제로는 Xcode에서 무엇이 제공되는지 살펴볼 필요가 있다.
마지막으로, 매크로가 가능하게 하는 광범위한 변화는 향후 1~2년 동안 Swift Evolution 자체가 진화할 것임을 의미할 수 있다. 이전에는 컴파일러의 광범위한 지원과 논의가 필요했던 많은 기능이 이제 매크로를 사용하여 프로토타입을 제작하고 출시할 수 있기 때문이다.
'Apple > Swift' 카테고리의 다른 글
[Swift 5.9] Consume operator to en the lifetime of a variable binding (4) | 2024.03.06 |
---|---|
[Swift 5.9] Noncopyable structs and enums (0) | 2024.01.11 |
[Swift 5.9] Value and Type Parameter Packs (0) | 2024.01.09 |
[Swift 5.9] if and switch expressions (1) | 2024.01.08 |
[Swift Basic] `@unknown` 키워드는 언제 쓰면 좋을까? (0) | 2024.01.05 |
- Total
- Today
- Yesterday
- Swift Conference
- AsyncSwift Korea Seminar
- swift5.9
- 꼼꼼한 재은 씨의 스위프트 문법편
- 이코테
- it seminar
- UITableViewCell
- 싱글톤
- ios
- 핵심내용
- 자바
- SWIFT
- CellStyle
- 의존성
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |