Notice
Recent Posts
Recent Comments
Link
관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - swift] Swift Macro 예제 - URLMacro 본문

Swift Macro

[iOS - swift] Swift Macro 예제 - URLMacro

jake-kim 2023. 9. 14. 01:39

(URLMacro 코드는 DougGregor Git repo를 참고하였습니다)

URLMacro 소개

  • 보통 URL(string:)을 사용하여 URL을 만들면 run time시점에 해당 string이 URL 형태인지 확인하여 Optional을 반환하지만 swift macro를 사용하면 compile time에 해당 string이 유효한 URL의 형태인지 파악이 가능
  • URLMacro의 목적
    • 잘못된 URL 형태의 문자열로 URL을 초기화할 때 컴파일 타임에 알 수 있도록 제공

URLMacro 구현

  • swift macro 프로젝트 생성 방법은 이전 포스팅 글 참고
  • attached 매크로와 freestanding 매크로 중에 URLMacro는 독립적으로 연산자처럼 사용되는 freestanding 매크로이며 expressionMacro이므로 아래처럼 선언
//  URLMacro.swift

import Foundation
import SwiftSyntax
import SwiftSyntaxMacros

public struct URLMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
        // TODO...
    }
}
  • 이전 포스팅 글에서 알아본 대로 SwiftSyntax에의해 문자열이 tree형태의 자료 구조로 저장되기 때문에 node로 부터 expression을 획득
    • expression은 사용하는쪽에서 입력한 문자열 그대로의 값
guard 
  let argument = node.argumentList.first?.expression
else { throw CustomError.message("#URL requires a static string literal") }
  • 사용하는쪽에서 콤마로 여러개의 문자열을 넘기는 경우도 있을 수 있으므로 단 하나의 문자열만 넘기는지 체크하는 조건도 추가
    • String 타입만 넘어오도록 String타입만 받는 조건도 추가
guard
    let argument = node.argumentList.first?.expression,
    let segments = argument.as(StringLiteralExprSyntax.self)?.segments,
    segments.count == 1,
    case .stringSegment(let literalSegment)? = segments.first
else {
    throw CustomError.message("#URL requires a static string literal")
}
  • 입력받은 URL string값을 URL(string:)에 넣어서 초기화 했을 때 nil이면 컴파일 에러를 발생하고, 만약 nil이 아니면 강제 unwrapping한 값을 리턴 
if URL(string: literalSegment.content.text) == nil {
    throw CustomError.message("malformed url: \(argument)")
} else {
    return "URL(string: \(argument))!"
}

(전체 코드)

import Foundation
import SwiftSyntax
import SwiftSyntaxMacros

public struct URLMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) throws -> ExprSyntax {
        
        guard
            let argument = node.argumentList.first?.expression,
            let segments = argument.as(StringLiteralExprSyntax.self)?.segments,
            segments.count == 1,
            case .stringSegment(let literalSegment)? = segments.first
        else {
            throw CustomError.message("#URL requires a static string literal")
        }
        
        if URL(string: literalSegment.content.text) == nil {
            throw CustomError.message("malformed url: \(argument)")
        } else {
            return "URL(string: \(argument))!"
        }
    }
}

Plugin에 위에서 정의한 URLMacro 추가

  • swift macro를 처음 만들면 StringifyMacro.self에 관한 매크로가 추가된 상태인데, 이 아래에 URLMacro.self도 추가
//  Plugins.swift

import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros

@main
struct ExMacrosPlugin: CompilerPlugin {
    let providingMacros: [Macro.Type] = [
        StringifyMacro.self,
        URLMacro.self
    ]
}

매크로의 인터페이스를 정의

  • freestanding(expression) 형태이며, 매크로의 이름은 #URL로 선언
    • 인터페이스 정의하는 방법은 구체적인 방법은 이전 포스팅 글 참고
// ExMacros.swift

@freestanding(expression) 
public macro URL(_ stringLiteral: String) -> URL = #externalMacro(module: "ExMacrosMacros", type: "URLMacro")

만든 매크로 테스트

  • 만들 매크로를 테스트 해볼 수 있는 main.swift에 작성
print(#URL("https://ios-development.tistory.com/"))
print(#URL("iOS 앱 개발 알아가기"))

(실행하면 기대동작 확인 완료)

  • 잘못된 형식의 URL 문자열에서 컴파일 에러가 발생

* 전체 코드: https://github.com/JK0369/URLMacro

* 참고

https://github.com/DougGregor/swift-macro-examples

Comments