Swift Macro와 SwiftSyntax로 코드 자동 생성 및 분석하기

작성일 :

Swift Macro와 SwiftSyntax로 코드 자동 생성 및 분석하기

Swift 프로그래밍 언어는 그 성능과 사용 편의성으로 인해 많은 개발자들 사이에서 인기를 얻고 있습니다. Swift는 또한 새로운 기능들을 빠르게 도입하여 개발자들에게 더 많은 도구와 기능을 제공하고 있습니다. 그 중 하나가 바로 Macro와 SwiftSyntax입니다. 이 글에서는 Swift MacroSwiftSyntax를 활용하여 코드 자동 생성 및 분석을 어떻게 수행할 수 있는지에 대해 알아보겠습니다.

Swift Macro란?

Macro는 반복적인 코드 생성을 자동화하는 기능입니다. 이는 코드 작성 속도를 크게 향상시킬 수 있으며, 코드의 일관성을 유지하는 데 도움이 됩니다. Swift에서 Macros는 강력한 기능을 제공하며, 이를 통해 코드 블록을 템플릿 형태로 만들고 필요한 곳에 삽입할 수 있습니다.

Macro 사용 예제

간단한 예제로, 로그 출력을 자동으로 추가하는 Macro를 만들어보겠습니다.

swift
#define LOG_METHOD [NSLog(@"%s", __PRETTY_FUNCTION__)]

void myFunction() {
   LOG_METHOD; // This will print the function name to the log
   // Rest of the code
}

이 예제에서는 로그 출력을 위한 간단한 Macro를 정의했습니다. 이제 함수 내에서 LOG_METHOD를 호출하면 자동으로 해당 함수 이름이 로그에 출력됩니다.

SwiftSyntax란?

SwiftSyntax는 Swift 코드를 구문 분석하고 변환하는 기능을 제공합니다. 이를 통해 코드 분석, 리팩토링, 그리고 도구 제작이 가능해집니다. SwiftSyntax는 Swift 코드를 구문 트리(Syntax Tree)로 표현하며, 이 트리를 조작하여 코드를 수정하거나 새로운 코드를 삽입할 수 있습니다.

SwiftSyntax 예제

아래는 SwiftSyntax를 사용하여 Swift 코드 파일을 분석하고 함수 이름을 추출하는 예제입니다.

swift
import SwiftSyntax

class FunctionVisitor: SyntaxVisitor {
    var functions: [String] = []

    override func visit(_ node: FunctionDeclSyntax) -> SyntaxVisitorContinueKind {
        if let identifier = node.identifier.tokenKind.text {
            functions.append(identifier)
        }
        return .skipChildren
    }
}

let url = URL(fileURLWithPath: "path/to/your/swiftfile.swift")
let sourceFile = try SyntaxParser.parse(url)

let visitor = FunctionVisitor()
visitor.walk(sourceFile)

print(visitor.functions)

이 예제에서는 FunctionVisitor 클래스를 만들어 함수 선언(FunctionDeclSyntax) 노드를 방문하며, 해당 노드의 함수 이름을 추출하여 리스트에 저장합니다. 이후, 대상 Swift 파일을 파싱하고, 결과적으로 함수 이름 목록을 출력합니다.

실전 예제: Model 코드를 자동 생성하기

Swift Macro와 SwiftSyntax를 결합하여 실제 개발에 유용한 기능을 만들어 보겠습니다. 이번 예제에서는 주어진 JSON 구조에 따라 Swift 모델 코드를 자동 생성하는 예제를 다룹니다.

JSON 파일 예

json
{
   "name": "String",
   "age": "Int",
   "isActive": "Bool"
}

위 JSON 파일에 대해 Swift 모델 코드를 생성할 수 있습니다.

모델 코드 자동 생성

swift
import SwiftSyntax

class ModelGenerator: SyntaxRewriter {
    var json: [String: String]

    init(json: [String: String]) {
        self.json = json
    }

    override func visit(_ node: SourceFileSyntax) -> Syntax {
        var properties: [DeclSyntax] = []

        for (key, type) in json {
            let property = SyntaxFactory.makeVariableDecl(
                attributes: nil,
                modifiers: nil,
                letOrVarKeyword: SyntaxFactory.makeLetKeyword().withLeadingTrivia(.spaces(4)),
                bindings: SyntaxFactory.makePatternBindingList([
                    SyntaxFactory.makePatternBinding(
                        pattern: SyntaxFactory.makeIdentifierPattern(
                            identifier: SyntaxFactory.makeIdentifier(key)),
                        typeAnnotation: SyntaxFactory.makeTypeAnnotation(
                            colon: SyntaxFactory.makeColonToken(),
                            type: SyntaxFactory.makeTypeIdentifier(type)),
                        initializer: nil,
                        accessor: nil,
                        trailingComma: nil)
                ])
            )
            properties.append(property)
        }

        let modelStruct = SyntaxFactory.makeStructDecl(
            attributes: nil,
            modifiers: nil,
            structKeyword: SyntaxFactory.makeStructKeyword().withLeadingTrivia(.spaces(0)),
            identifier: SyntaxFactory.makeIdentifier("GeneratedModel"),
            genericParameterClause: nil,
            inheritanceClause: nil,
            genericWhereClause: nil,
            members: SyntaxFactory.makeMemberDeclBlock(
                leftBrace: SyntaxFactory.makeLeftBraceToken(),
                members: SyntaxFactory.makeMemberDeclList(properties),
                rightBrace: SyntaxFactory.makeRightBraceToken())
        )

        return SyntaxFactory.makeSourceFile(
            statements: SyntaxFactory.makeCodeBlockItemList([modelStruct]),
            eofToken: node.eofToken
        )
    }
}

let json = ["name": "String", "age": "Int", "isActive": "Bool"]
let generator = ModelGenerator(json: json)
let sourceFile = SyntaxFactory.makeSourceFile
let generatedCode = generator.visit(sourceFile)

print(generatedCode)

이 예제는 JSON 파일의 구조에 맞춰 Swift 모델 코드를 자동 생성하는 내용입니다. ModelGenerator 클래스는 SyntaxRewriter를 상속받아 visit 메소드를 재정의하여 모델의 속성을 생성하고, 이를 새로운 Swift 소스 파일로 반환합니다.

결론

Swift Macro와 SwiftSyntax는 개발자가 반복적인 작업을 자동화하고, 쉽게 코드를 분석 및 변환할 수 있도록 도와주는 강력한 도구입니다. 이 글에서는 두 가지 기능을 소개하고, 실제로 어떻게 활용할 수 있는지 예제를 통해 설명했습니다. 적절히 활용한다면 생산성을 크게 향상시킬 수 있는 기능들입니다.