24 December 2023

이번 글에서는 빌드 없이도 Macro를 사용할 수 있는 방법을 소개하겠습니다.

Swift Package에서는 Macro를 다음과 같이 정의합니다:

import PackageDescription
import CompilerPluginSupport
import Foundation

let package = Package(
    name: "MyMacro",
    platforms: [.macOS(.v10_15), .iOS(.v13)],
    ...
    targets: [
        .macro(
            name: "MyMacroMacros",
            dependencies: [
                .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
                .product(name: "SwiftCompilerPlugin", package: "swift-syntax")
            ]
        ),
        ...

첫 번째 단계로, MyMacroMacros를 빌드하여 .build/arm64-apple-macosx/debug 경로에 바이너리를 생성합니다.

$ swift build --product MyMacroMacros


생성된 Macro 바이너리는 -load-plugin-executable 컴파일 옵션을 통해 사용할 수 있습니다.

이를 위해 Command Line Tool을 이용해 프로젝트를 생성하고, 이전에 생성한 MyMacroMacros 바이너리를 프로젝트 경로에 추가합니다.



다음으로, Build SettingsOTHER_SWIFT_FLAGS-load-plugin-executable 옵션을 추가하고, 여기에 MyMacroMacros 모듈 경로를 지정합니다.

-load-plugin-executable ${SRCROOT}/MyCommand/MyMacroMacros#MyMacroMacros


소스코드에서는 MyMacroMacrosStringifyMacro를 연결하고, 빌드 후 실행하여 정상적으로 결과가 출력되는지 확인합니다.

/// FileName : main.swift
import Foundation

@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MyMacroMacros", type: "StringifyMacro")

let a = 17
let b = 25

let (result, code) = #stringify(a + b)

print("The value \(result) was produced by the code \"\(code)\"")



이제, Dynamic Framework에서 Macro를 정의하고 사용하는 방법을 살펴보겠습니다.

Macro를 모듈 간에 사용할 수 있도록 의존 관계를 구축합니다.

graph TD; App-->ModuleA-->MacroKit;



MacroKit에서 MyMacroMacros를 복사합니다. Build Phases에서 Copy Files Phase를 추가하여 MyMacroMacros 바이너리를 Build Production Directory에 추가하고, 이를 소스 코드 컴파일 이전에 복사되도록 설정합니다.


MacroKitBuild Settings에서는 OTHER_SWIFT_FLAGS-load-plugin-executable 옵션을 추가하고, Macro 경로로 ${BUILT_PRODUCTS_DIR}/MyMacroMacros#MyMacroMacros를 지정합니다.

OTHER_SWIFT_FLAGS = -load-plugin-executable ${BUILT_PRODUCTS_DIR}/MyMacroMacros#MyMacroMacros


다른 모듈에서도 Macro를 사용하기 위해서는, MyMacroMacros 바이너리의 경로를 지정해야 합니다. 빌드 시 MyMacroMacros 바이너리를 복사해두면, 다른 모듈에서 ${BUILT_PRODUCTS_DIR}에 있는 경로를 쉽게 지정할 수 있습니다.

이어서, MacroKit 모듈에 Marco.swift 파일을 추가하고 매크로를 정의합니다.

/// Module : MacroKit
/// FileName : Macro.swift

import Foundation

@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MyMacroMacros", type: "StringifyMacro")


FeatureA 모듈에서는 MacroKit 모듈을 추가하고, Build Settings에서 -load-plugin-executable ${BUILT_PRODUCTS_DIR}/MyMacroMacros#MyMacroMacros 옵션을 지정합니다. 이를 통해 FeatureA 모듈에서 MacroKit 모듈을 import하고 stringify 매크로를 사용할 수 있습니다.


import Foundation
import MacroKit

public struct ServiceA {
    public static func run() {
        let a = 17
        let b = 25
        
        let (result, code) = #stringify(a + b)
        
        print(#file, "The value \(result) was produced by the code \"\(code)\"")
    }
}

마지막으로, App에서도 MacroKit 모듈을 추가하고, Build Settings에서 -load-plugin-executable ${BUILT_PRODUCTS_DIR}/MyMacroMacros#MyMacroMacros 옵션을 지정합니다.


이제 App에서도 stringify 매크로를 사용할 수 있습니다.


이제 SampleApp을 실행하면, 콘솔에서 Macro가 성공적으로 실행되는 것을 확인할 수 있습니다.


이러한 방법을 통해, Swift Package뿐만 아니라 직접 Macro를 다루고 다른 모듈에서 쉽게 사용할 수 있는 방법을 알아보았습니다.


위 코드의 샘플은 여기에서 확인하실 수 있습니다.