🔥 매크로 개발과 디버깅

434자
5분

매크로는 외부 상태에 의존하지 않고 AST(Abstract Syntax Tree)를 변환하기 때문에 테스트를 통한 개발에 매우 적합합니다. 게다가 문자열 리터럴로부터 구문 노드를 만들 수 있어서 테스트를 위한 입력 설정이 간단해집니다. 또한 AST의 description 속성을 읽어서 예상 값과 비교할 문자열을 얻을 수 있죠.

이전 섹션에서 다뤘던 #fourCharacterCode 매크로의 테스트 예제를 살펴봅시다:

let source: SourceFileSyntax =
    """
    let abcd = #fourCharacterCode("ABCD")
    """
// 테스트할 소스 코드를 문자열 리터럴로 정의합니다.
 
let file = BasicMacroExpansionContext.KnownSourceFile(
    moduleName: "MyModule",
    fullFilePath: "test.swift"
)
// 소스 파일에 대한 정보를 담은 KnownSourceFile 객체를 생성합니다.
 
let context = BasicMacroExpansionContext(sourceFiles: [source: file])
// 매크로 확장 컨텍스트를 생성하고, 테스트할 소스 파일을 등록합니다.
 
let transformedSF = source.expand(
    macros:["fourCharacterCode": FourCharacterCode.self],
    in: context
)
// 매크로를 확장하여 변환된 소스 파일을 얻습니다.
 
let expectedDescription =
    """
    let abcd = 1145258561 as UInt32
    """
// 예상되는 변환 결과를 문자열로 정의합니다.
 
precondition(transformedSF.description == expectedDescription)
// 변환 결과와 예상 결과가 일치하는지 precondition으로 검사합니다.
swift

위 예제에서는 precondition을 사용해 매크로를 테스트했지만, 테스트 프레임워크를 사용할 수도 있습니다.

매크로 개발 과정에서 디버깅이 필요할 때가 있습니다. Xcode에서는 다음과 같은 방법으로 매크로를 디버깅할 수 있어요:

  1. 브레이크포인트를 설정하세요.
    • 매크로 코드에서 중단점을 설정하면 실행이 해당 지점에서 일시 중지됩니다.
  2. 디버깅 콘솔을 활용하세요.
    • print() 문을 사용해 변수 값을 출력하고 실행 흐름을 추적할 수 있습니다.
    • dump() 함수로 AST 노드의 내용을 자세히 출력해 볼 수도 있죠.
  3. LLDB 명령어를 사용하세요.
    • po 명령으로 변수나 표현식의 값을 출력할 수 있습니다.
    • fr v 명령으로 현재 프레임의 변수 목록을 확인할 수 있습니다.

다음은 LLDB에서 매크로 디버깅 예시입니다:

(lldb) breakpoint set -f FourCharacterCode.swift -l 10
Breakpoint 1: where = MyModule`FourCharacterCode.expansion(of:in:) at FourCharacterCode.swift:10:24, address = 0x00000001000015e0

(lldb) process launch
Process 12345 launched: '/path/to/MyApp' (x86_64)
Process 12345 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x00000001000015e0 MyModule`FourCharacterCode.expansion(of:in:) at FourCharacterCode.swift:10:24
   7    public static func expansion(
   8        of node: some FreestandingMacroExpansionSyntax,
   9        in context: some MacroExpansionContext
-> 10       ) -> ExprSyntax {
   11           let charactersExpr = node.argumentList.first!.expression
   12           let charactersString = charactersExpr.as(StringLiteralExprSyntax.self)!
   13
Target 0: (MyApp) stopped.

(lldb) po charactersExpr
StringLiteralExpr("ABCD")

(lldb) fr v
(some FreestandingMacroExpansionSyntax) node = 0x000060000056e250 {
  __storage = {
    opaque: 0x60000002b300
    pointer: 0x000060000056e250
  }
}
(some MacroExpansionContext) context = 0x00007ecd7e16ee80 {
  __storage = 0x00007ecd7e16ee80
}
(some ExprSyntax) charactersExpr = 0x0000600000576260 {
  __storage = {
    opaque: 0x60000002b700
    pointer: 0x0000600000576260
  }
}
text

이렇게 디버깅 도구를 활용하면 매크로 개발 과정에서 발생하는 문제를 효과적으로 해결할 수 있습니다.

매크로 개발은 컴파일러와 밀접한 관련이 있기 때문에 Swift 컴파일러의 동작을 잘 이해하는 것이 중요합니다. 특히 AST가 어떻게 구성되고 변환되는지 파악해야 하죠. 그래야 의도한 대로 코드를 생성하는 매크로를 만들 수 있습니다.

이상으로 Swift 매크로 개발과 디버깅에 대해 알아보았습니다. 앞으로도 매크로를 활용해 더욱 강력하고 표현력 있는 Swift 코드를 작성할 수 있기를 바랍니다!