Custom Codable Macro #1 세팅

김가영·2025년 3월 21일
0

swift

목록 보기
8/9

시작하기

  • 최소 Xcode15, Swift5.9 가 필요하다.
  • Xcode에서 패키지 생성을 클릭
  • Testing은 XCTest, 이름은 CodableMacro로 추가해줬다.
  • 생성되는 기본 프로젝트는 아래와 같다.

Package.swift

  • swift의 패키지는, .xcodeproj.xcworkspace 대신 폴더구조+package manifest 로만 구성된다.
    • target 이름과 folder 이름이 일치해야 하며, 파일들이 추가된 폴더가 자동으로 해당 파일의 target이 된다.
  • 패키지 이름에 Macro가 포함되니 CodableMacroMacros 라는 폴더가 추가됐다. 보기 싫어서 기본으로 설정된 package 파일을 아래와 같이 변경해줬다.
// swift-tools-version: 5.9
// swift 5.9를 지원하기 위해 변경

import PackageDescription
import CompilerPluginSupport

let package = Package(
    name: "CodableMacro",
    platforms: [.macOS(.v13), .iOS(.v15), .watchOS(.v6), .macCatalyst(.v13)], // *불필요한 platform 제외
    products: [
        .library(
            name: "CodableMacro",
            targets: ["CodableMacro"]
        ),
        // *executable은 제외해줬다. 이렇게 하면 package import 시 CodableMacroClient에 있는 main 파일은 실행할 수 없다.
    ],
    dependencies: [
        .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0-latest"),
    ],
    targets: [
        .macro(
            name: "CodableMacroCore",
            dependencies: [
                .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
                .product(name: "SwiftCompilerPlugin", package: "swift-syntax")
            ]
        ),
        .target(name: "CodableMacro", dependencies: ["CodableMacroCore"]),
        .executableTarget(name: "CodableMacroClient", dependencies: ["CodableMacro"]),
        .testTarget(
            name: "CodableMacroTests",
            dependencies: [
                "CodableMacroCore",
                .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
            ]
        ),
    ]
)
  • 앞서 말했듯 스위프트 패키지에서 파일이름과 타겟이름은 동일해야한다. 타겟 이름을 CodableMacroMacro -> CodableMacroCore로 변경해줬기 때문에 폴더 이름과 파일 이름도 바꿔줬다.
  • 추가로 CodableMacro에서 아래처럼 모듈이름을 수정해줘야 한다.
// AS-IS
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "CodableMacroMacros", type: "StringifyMacro")

// TO-BE
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "CodableMacroCore", type: "StringifyMacro")
  • 잘 수정되었는지 확인하려면, scheme을 client로 바꿔서 mac에서 실행시켜보면 된다.
    정상적으로 모듈명이 변경되었다면 스위프트가 기본으로 추가해준 코드인 The value 42 was produced by the code "a + b" 가 console에 출력된다!

패키지 구조

패키지를 생성하면 기본으로 #stringify 매크로가 작성되어 있을 거다. 이걸 이용해서 대강 패키지의 구조와 매크로가 작성 순서를 알아볼 수 있다.

CodableMacro

// CodableMacro.swift
// 실제로 외부에 제공되는 인터페이스들 정의
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "CodableMacroCore", type: "StringifyMacro")
  • 매크로가 정의되어 있는 폴더다.
  • 매크로 외에, 패키지에서 제공하고 싶은 public 타입이나 메서드들도 여기 폴더 안에 추가하면 된다.
  • #externalMacro(module: "CodableMacroCore", type: "StringifyMacro") 는 매크로 구현부는 CodableMacroCore 폴더(모듈) 안의 StringfyMacro 타입을 참고하라고 말해주는거다.

CodableMacroCore

// CodableMacroCore.swift
// 실제 #stringify 매크로의 구현부
import SwiftCompilerPlugin // Swift 컴파일러와 연결되어 매크로 기능을 등록하고 실행할 수 있게 해준다.
import SwiftSyntax // 소스 코드를 Sytax Tree 구조로 표현해준다.
import SwiftSyntaxBuilder // Syntax Tree 구성을 위한 편리 API를 제공한다.
import SwiftSyntaxMacros // 매크로 작성에 필요한 프로토콜과 타입을 제공한다.

public struct StringifyMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) -> ExprSyntax {
        guard let argument = node.arguments.first?.expression else {
            fatalError("compiler bug: the macro does not have any arguments")
        }

        return "(\(argument), \(literal: argument.description))"
    }
}

/// 매크로 기능을 Swift 컴파일러에 등록하는 진입점
/// @main은 이 구조체가 프로그램의 시작점임을 나타낸다.
///
/// CompilerPlugin을 채택하여:
/// 1. 컴파일러가 이 매크로 패키지를 플러그인으로 인식하게 하고
/// 2. providingMacros 배열을 통해 사용 가능한 매크로 목록을 컴파일러에 알린다.
///
/// 새로운 매크로를 추가하려면 providingMacros 배열에 해당 매크로 타입을 추가해야한다.
@main
struct CodableMacroPlugin: CompilerPlugin {
    let providingMacros: [Macro.Type] = [
        StringifyMacro.self,
    ]
}
  • 매크로의 구현이 실제로 작성되는 폴더다.
  • 새로운 매크로를 추가하려면 providingMacros 배열에 해당 매크로 타입을 추가해야한다.

CodableMacroClient

// CodableMacroClient로 scheme을 변경하면 테스트 가능
import CodableMacro

let a = 17
let b = 25

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

print("The value \(result) was produced by the code \"\(code)\"")
  • 실행 가능한 파일

Tests > CodableMacroTests

  • 테스트 파일들이 존재한다.
  • 매크로의 구현 자체는 컴파일 타임에 검증할 수 없기 때문에, 매크로가 실제로 의도한대로 코드를 확장했는지 하나하나 확인하거나 통합테스트를 작성하여 매크로를 검증하게 된다.
  • SwiftSyntax는 일반 코드보다 가독성이 낮고 컴파일 타임 검증이 안되므로 테스트 코드 작성이 보다 더 중요한 것 같았다.
  • TDD로 프로젝트를 진행해보려고 한다.
profile
개발블로그

0개의 댓글

관련 채용 정보