티스토리 뷰

Apple/Swift

[Swift 5.9] Macro

doyeonjeong_ 2024. 1. 11. 01:07

원문 : https://www.hackingwithswift.com/swift/5.9/macros

 

SE-0382, SE-0389, SE-0397이 결합되어 컴파일 시 구문을 변환하는 코드를 생성할 수 있는 '매크로' 기능

 

C++에서 매크로는 코드를 전처리하는 방법으로, 메인 컴파일러가 코드를 보기 전에 코드의 텍스트 교체를 효과적으로 수행하여 손으로 작성하고 싶지 않은 코드를 생성할 수 있다.

Swift의 매크로는 이와 비슷하지만 훨씬 더 강력하며, 따라서 훨씬 더 복잡하다. 또한 매크로를 사용하면 프로젝트의 Swift 코드가 컴파일되기 전에 동적으로 조작할 수 있으므로 컴파일 시점에 추가 기능을 삽입할 수 있다.

 

핵심 사항:

  • 매크로는 단순한 문자열 대체가 아닌 유형 안전성이 있으므로 매크로가 작동할 데이터를 정확히 알려주어야 한다.
  • 매크로는 빌드 단계에서 외부 프로그램으로 실행되며 기본 앱 타겟에 존재하지 않는다.
  • 매크로는 단일 표현식을 생성하는 ExpressionMacro, 게터와 세터를 추가하는 AccessorMacro, 타입을 프로토콜에 맞게 만드는 ConformanceMacro와 같이 여러 가지 작은 유형으로 나뉜다.
  • 매크로는 파싱된 소스 코드와 함께 작동하며, 조작 중인 프로퍼티의 이름이나 유형, 구조체 내부의 다양한 프로퍼티 등 코드의 개별 부분을 쿼리할 수 있다.
  • 매크로는 샌드박스 내에서 작동하며 지정된 날짜에만 작동해야 한다.

특히 마지막 부분이 중요한데, Swift의 매크로 지원은 소스 코드를 이해하고 조작하기 위해 Apple의 SwiftSyntax 라이브러리를 기반으로 한다. 이 라이브러리를 매크로의 종속성으로 추가해야 한다.

간단한 매크로부터 시작하여 매크로가 어떻게 작동하는지 살펴보자. 매크로는 컴파일 시 실행되므로 앱이 빌드된 날짜와 시간을 반환하는 작은 매크로를 만들 수 있으며, 이는 디버그 진단에 유용하게 사용할 수 있다. 이 작업에는 여러 단계가 필요하며, 그 중 몇 단계는 기본 대상과 별도의 모듈에서 수행해야 한다.

먼저 매크로 확장을 수행하는 코드, 즉 #buildDate2023-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 자체가 진화할 것임을 의미할 수 있다. 이전에는 컴파일러의 광범위한 지원과 논의가 필요했던 많은 기능이 이제 매크로를 사용하여 프로토타입을 제작하고 출시할 수 있기 때문이다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
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
글 보관함