🔥 매크로 구현
Swift 5.9부터는 매크로를 사용하여 코드를 더욱 간결하고 표현력 있게 작성할 수 있습니다. 매크로를 구현하려면 두 가지 컴포넌트가 필요한데요. 하나는 매크로 확장을 수행하는 타입이고, 다른 하나는 매크로를 API로 노출하는 라이브러리입니다. 이 두 부분은 매크로를 사용하는 코드와 별도로 빌드되며, 매크로 구현은 매크로 클라이언트를 빌드할 때 실행됩니다.
Swift Package Manager를 사용하여 새 매크로를 만들려면 swift package init --type macro
명령을 실행하면 됩니다. 이렇게 하면 매크로 구현과 선언을 위한 템플릿을 포함한 여러 파일이 생성됩니다.
기존 프로젝트에 매크로를 추가하려면 Package.swift
파일의 시작 부분을 다음과 같이 수정합니다:
swift-tools-version
주석에서 Swift 도구 버전을 5.9 이상으로 설정합니다.CompilerPluginSupport
모듈을 임포트합니다.platforms
목록에 macOS 10.15를 최소 배포 대상으로 포함시킵니다.
아래 코드는 Package.swift
파일의 시작 부분 예시를 보여줍니다:
// swift-tools-version: 5.9 import PackageDescription import CompilerPluginSupport let package = Package( name: "MyPackage", platforms: [ .iOS(.v17), .macOS(.v13)], // ... )
swift
다음으로, 기존 Package.swift
파일에 매크로 구현을 위한 타겟과 매크로 라이브러리를 위한 타겟을 추가합니다. 예를 들어, 아래와 같이 추가할 수 있으며, 이름은 프로젝트에 맞게 변경하면 됩니다:
targets: [ // 소스 변환을 수행하는 매크로 구현 .macro( name: "MyProjectMacros", dependencies: [ .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), .product(name: "SwiftCompilerPlugin", package: "swift-syntax") ] ), // 매크로를 API의 일부로 노출하는 라이브러리 .target(name: "MyProject", dependencies: ["MyProjectMacros"]), ]
swift
위 코드는 두 개의 타겟을 정의합니다: MyProjectMacros
는 매크로의 구현을 포함하고, MyProject
는 해당 매크로를 사용 가능하게 만듭니다.
매크로 구현은 SwiftSyntax 모듈을 사용하여 AST(Abstract Syntax Tree)를 통해 Swift 코드와 구조화된 방식으로 상호 작용합니다. Swift Package Manager로 새 매크로 패키지를 생성한 경우, 생성된 Package.swift
파일에 SwiftSyntax에 대한 의존성이 자동으로 포함됩니다. 기존 프로젝트에 매크로를 추가하는 경우, Package.swift
파일에 SwiftSyntax에 대한 의존성을 추가해야 합니다:
dependencies: [ .package(url: "<https://github.com/apple/swift-syntax>", from: "509.0.0") ],
swift
매크로의 역할에 따라 SwiftSyntax에서 제공하는 해당 프로토콜을 매크로 구현이 채택해야 합니다. 예를 들어, 이전 섹션에서 살펴본 #fourCharacterCode
매크로를 구현하는 구조체는 다음과 같습니다:
import SwiftSyntax import SwiftSyntaxMacros public struct FourCharacterCode: ExpressionMacro { public static func expansion( of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext ) throws -> ExprSyntax { guard let argument = node.argumentList.first?.expression, let segments = argument.as(StringLiteralExprSyntax.self)?.segments, segments.count == 1, case .stringSegment(let literalSegment)? = segments.first else { throw CustomError.message("Need a static string") } let string = literalSegment.content.text guard let result = fourCharacterCode(for: string) else { throw CustomError.message("Invalid four-character code") } return "\(raw: result) as UInt32" } } private func fourCharacterCode(for characters: String) -> UInt32? { guard characters.count == 4 else { return nil } var result: UInt32 = 0 for character in characters { result = result << 8 guard let asciiValue = character.asciiValue else { return nil } result += UInt32(asciiValue) } return result } enum CustomError: Error { case message(String) }
swift
각 코드에 대한 설명은 다음과 같습니다:
public struct FourCharacterCode: ExpressionMacro {
swift
FourCharacterCode
구조체는ExpressionMacro
프로토콜을 채택합니다.
public static func expansion( of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext ) throws -> ExprSyntax {
swift
expansion(of:in:)
메서드는 AST를 확장하는 역할을 합니다.- 매크로가 사용된 코드의 AST와 컨텍스트가 메서드의 인자로 전달됩니다.
guard let argument = node.argumentList.first?.expression, let segments = argument.as(StringLiteralExprSyntax.self)?.segments, segments.count == 1, case .stringSegment(let literalSegment)? = segments.first else { throw CustomError.message("Need a static string") }
swift
- 첫 번째
guard
블록은 AST에서#fourCharacterCode
에 전달된 문자열 리터럴을 추출하여literalSegment
에 할당합니다. - 매크로 사용이 잘못된 경우 에러를 던집니다.
let string = literalSegment.content.text guard let result = fourCharacterCode(for: string) else { throw CustomError.message("Invalid four-character code") }
swift
- 두 번째
guard
블록은fourCharacterCode(for:)
함수를 호출하여 문자열에 해당하는 32비트 부호 없는 정수 리터럴 값을 계산합니다.
return "\(raw: result) as UInt32"
swift
expansion(of:in:)
메서드는 AST에서 표현식을 나타내는ExprSyntax
인스턴스를 반환합니다.ExprSyntax
타입은StringLiteralConvertible
프로토콜을 채택하므로, 문자열 리터럴을 사용하여 결과를 생성할 수 있습니다.
기존 Swift Package Manager 프로젝트에 이 매크로를 추가하는 경우, 매크로 타겟의 진입점 역할을 하고 타겟에서 정의한 매크로를 나열하는 타입을 추가해야 합니다:
import SwiftCompilerPlugin @main struct MyProjectMacros: CompilerPlugin { var providingMacros: [Macro.Type] = [FourCharacterCode.self] }
swift
#fourCharacterCode
매크로를 확장하기 위해 Swift는 이 매크로를 사용하는 코드의 AST를 매크로 구현이 포함된 라이브러리로 전송합니다. 라이브러리 내에서 Swift는 FourCharacterCode.expansion(of:in:)
메서드를 호출하고, AST와 컨텍스트를 메서드의 인자로 전달합니다. expansion(of:in:)
메서드의 구현은 #fourCharacterCode
에 인자로 전달된 문자열을 찾아 해당하는 32비트 부호 없는 정수 리터럴 값을 계산합니다.
매크로 구현 시 유의할 점은 매크로가 잘못 사용된 경우 에러를 던지는 것입니다. 에러 메시지는 잘못된 호출 위치에서 컴파일러 에러가 됩니다. 예를 들어, #fourCharacterCode("AB" + "CD")
와 같이 매크로를 호출하려고 하면 컴파일러는 "Need a static string"이라는 에러를 표시합니다.
expansion(of:in:)
메서드는 ExprSyntax
인스턴스를 반환하는데, 이는 AST에서 표현식을 나타내는 SwiftSyntax의 타입입니다. 이 타입은 StringLiteralConvertible
프로토콜을 채택하므로, 매크로 구현에서는 문자열 리터럴을 사용하여 간편하게 결과를 생성할 수 있습니다. 매크로 구현에서 반환하는 모든 SwiftSyntax 타입은 StringLiteralConvertible
을 채택하므로, 어떤 종류의 매크로를 구현할 때도 이 접근 방식을 사용할 수 있습니다.
매크로를 활용하면 반복적인 코드를 줄이고 더 간결하고 표현력 있는 코드를 작성할 수 있습니다. SwiftSyntax와 함께 사용하면 강력한 코드 생성과 변환 도구를 만들 수 있죠. 특히 도메인 특화 언어(DSL)를 만들거나 코드를 자동으로 최적화할 때 매우 유용합니다.