[Swift] 매크로

매크로요..? 수강 신청 매크로 말고요..????

naljin
51 min readSep 30, 2023

들어가기 전에

흐음~~ 이번 내용은 올릴까 말까~~ 하다가 9월에 올린 포스팅이 없어서리,, 영상도 업로드된 김에 매크로 발표 준비하면서 정리했던 내용 발행갑니다 ~. ~ (사실상 WWDC 발라먹기..ㅋㅎ)

근데 6월에 내용 준비한 이후로, 9월까지 업데이트 된 내용은 크게 담겨있지 않아서 그 점은 감안해 주시고여..? (귀찮음 이슈 ㅎ)

구성 같은건 발표가 좀 더 다듬어진 버전이라 영상으로 보시는게 더 괜찮을 것 같지만서두,, 뭐.. 글에는 뭐 발표에서 시간상 빠졌던 내용도 있으니까~~ 맘에드시는거 보기로 하고 그럼 ㄱㄱㄱ!!

개-하!

덥덥23 을 보지 않으셨더라도, 나,, 조금 관심을 가지고 있을지두..? 하시는 분들이라면 “매크로” 라는 키워드를 한번씩 들어보셨을 것 같습니다.

웅성웅성👥👤👥뭐야..👤👥👥👤👥👥👤스위프트에서👥👤👥👤👥👤매크로…👥👤👥👤👥👤👥웅성웅성,,👤👥👥👤👥👤생겼대…👤👥👤👥👥 뭐야..👤👥👤👥👤👤👥👥👤👥웅성웅성..👤👥 👥👤👥👥👤👥 👤👥👤

핵심부터 말하자면, Swift 5.9 부터 매크로 기능이 도입되었다!! 는 건데요 오늘은 이 매크로에 대해 알아보도록 하겠습니다.

그 전에

🤔 : 그래서 매크로가 뭔데??

라는 의문부터 드시는 분도 있을거예요

매크로의 정의

매크로라는 단어는 익숙하지만 해당 정의에 대해서는 깊게 생각해보지 않았을 수도 있을 것 같습니다. 누구 얘기냐고요? 당연히 제 얘기인데요 ㅎㅎ 일단 macro 라는 단어부터 살펴보겠습니다.

macro 는 접두사로 붙으면, 크거나 대규모임을 나타낸다고해요. 흠.. 반의어인 micro 는 많이 들어봤는데 말이죠 ㅎㅎ..

그래서 거시 경제, 미시 경제를 영어로 macro-economy, micro-economy 라고 한대요,, 덜덜,,

그러면 이 macro 단어의 뜻을 기억한채로, 컴퓨터 과학에서 말하는 매크로의 정의를 냅다 긁어와볼까요?

우욱,, 너무 기니까 중요한 키워드만 뽑아내보겠습니다

키워드 위주로 보면, 매크로는 특정 입력을, 특정 출력으로 매핑해주는 규칙 혹은 패턴을 뜻한다고 합니다.

이러한 기능이 처음 도입된 Lisp 에서는 이 매핑 과정을 원래 코드를 크게 확장한다는 의미에서 macroexpand, 즉 매크로 확장이라고 불렀고, 확장에 사용되는 규칙을 macro 라고 불렀는데, 이후 언어들에서도 이 용어로 굳어졌다고 합니다.

도입 이유

여기까지 “매크로? 조그만 코드를 확장해서 커다란 코드 블록으로 만드는거 ㅇㅋㅇㅋ” 하고 이해했습니다. 그렇다면 애플은 도대체 왜 이 매크로를 Swift 에 도입한걸까요??

기본적으로 Swift 는 개발자가 표현적인 코드와 API를 작성할 수 있도록 돕기 위해 노력합니다.

🤔 : 너네가.. 언제…?

ㅎㅎ 예를 들어볼까요? 여기 Codable 을 conform 하는 Smoothie struct 가 있습니다.

struct Smoothie: Codable {
let id, title, description: String
var measuredIngredients: [MeasuredIngredient]
}

그리고 이렇게 선언이 끝납니다!!

하지만 여기서 뭔가 이상하지 않나요..? 잘 생각해보면 CodableDecodableEncodable protocol 을 conform 하고 있는 type 을 뜻하고

EncodableDecodable 에는 각각 필수로 구현해야하는 함수가 있는데,, 어떻게 저것만으로 선언이 끝나는 걸까요?

Encodable
Decodable

두둥, 여기서 우리의 짱위프트가 제 역할을 합니다. Codable 을 conform 할때 멤버에 대한 구현을 제공하지 않으면 Swift는 자동으로 해당 conformance 를 확장해서 프로그램에 삽입합니다. 아래 회색 상자가 그 확장에 대한 내용입니다.

이렇게 코드를 생성해주면, Codable을 사용할 때 정확히 어떻게 작동하는지 알 필요 없이 사용할 수 있으며, 추가적인 코드 작성에 대한 번거로움이 줄어듭니다.

Swift에는 이와 같은 방식으로 작동하는 많은 기능이 있습니다. 간단한 구문을 작성하면 컴파일러가 자동으로 보다 복잡한 코드로 확장해 줍니다

하지만 기존의 기능으로는 원하는 작업을 수행할 수 없는 경우 어떻게 해야 할까요? 그렇다면 Swift 컴파일러에 기능을 추가할 수도 있습니다. Swift 컴파일러는 오픈 소스이기 때문에 충분히 가능합니다.

Swift 컴파일러 자체에 내가 원하는 코드를 추가하기 위해서는, Swift 프로젝트 리더들과 기능에 대해 논의하고,, 화상 회의도 참여하고,, 하면 되는디요!

🙄 : 이걸요…..? 제가요…..?

벌써 머리 아프죠? ㅋㅎ 따라서 이 과정은 확장성 있는 프로세스는 아닙니다.

그래서 등장했습니다! 우리의 매크로!

Swift 의 매크로는 원하는 기능을 지원하기 위해서 컴파일러를 수정하는 방향이 아닌, Swift 패키지로 배포하는 방법을 택합니다. 따라서 자신만의 원하는 기능을 쉽게 추가해서 사용할 수 있고, 이를 통해 번거로움과 반복적인 코드를 줄일 수 있습니다.

이따가 다시 보겠지만, 우리는 매크로를 만들때 실제로 이렇게 File > New > Package 에서 Swift Macro 를 선택해서 만들게 됩니다. SPM 을 만들때와 비슷하죠?

디자인 철학

그러면 이 매크로를 만들때 애플 개발자들이 “우리는 매크로를 이런 특징을 가진 애로 만들거야~~” 라며 세운 목표들이 있을텐데요!

매크로 설계 목표는 크게 네가지로 나뉘며, 이제부터 하나씩 알아보도록 하겠습니다.

첫 번째 목표는 “매크로를 사용할 때, 이게 매크로다!! 라는 것을 명확하게 알 수 있어야 한다!”는 것입니다.

매크로에는 두 종류가 있습니다

  1. freestanding 매크로 - 항상 샵(#) 기호로 시작
  2. attached 매크로 - 항상 at(@) 기호로 시작

이 두가지 매크로의 종류는 어차피 뒤에서 자세하게 알아볼 것이므로 지금 뭐 이름을 기억해두실 필요도 없습니다. 중요한건 매크로에는 # 이나 @ 이 무조건 붙기 때문에, 이 기호들이 붙어있지 않은 코드라면, 매크로가 아니구나! 라고 생각할 수 있다는거죠.

두 번째 목표는 “매크로에 전달 및 반환되는 코드가 완전하고 오류가 없는지 확인되어야 한다”는 것입니다.

예를 들어 매크로에 1 + 와 같은 불완전한 표현식을 전달할 수 없습니다. 인자는 완전한 표현식이어야 합니다.

또한 잘못된 형식의 인자를 전달할 수 없습니다. 왜냐하면 매크로의 인자 및 결과는 함수와 마찬가지로 타입 체크가 이루어지기 때문입니다.

이런식으로 매크로의 구현은 입력값의 유효성을 검사하고, 잘못된 경우 컴파일러 경고나 오류를 발생시킬 수 있으므로 매크로를 올바르게 사용하는 것을 더욱 확신할 수 있습니다.

세 번째 목표는 “매크로 확장이 예측 가능하고 추가적인(addictive) 방식으로 프로그램에 통합되어야 한다”는 것입니다.

매크로는 프로그램의 가시적인 코드에만 추가할 수 있습니다. 코드를 삭제하거나 변경할 수는 없습니다.

어려운 말로 설명했지만, 사실 아래와 같은 코드에서 #someUnknownMacro이 무슨 일을 하는지는 몰라도, 적어도 finishDoingThingy에 대한 호출이 삭제되거나 새로운 함수로 이동되지 않는다는 것을 알 수 있습니다.

마지막 목표는 “매크로가 파악할 수 없는 어떤 마법이 되어서는 안된다”는 것입니다.

매크로는 단순히 프로그램에 더 많은 코드를 추가하는 것이며, 이는 Xcode에서 바로 확인할 수 있습니다. 매크로의 사용 위치를 마우스 오른쪽 버튼으로 클릭하고, 해당 매크로가 어떻게 확장되는지 확인할 수 있습니다. 확장된 코드 내부에 브레이크 포인트를 설정하거나 여기서 디버거로 진입할 수도 있습니다.

패키지 생성 for 매크로

이제 실제 코드를 살펴 볼까여?

매크로 작성에 적합한 상황으로, 아래와 같은 코드가 있다고 가정해보겠습니다.

결과 값 — 식

이 코드의 기능은 사칙 연산의 식과, 결과 값을 나타내는 것입니다. 반대로 되어있어서 헷갈리긴 하지만, 튜플의 왼쪽에는 Int 로 된 결과 값이, 오른쪽에는 String 으로 된 계산 식이 있습니다. 뭔가 구몬 같기도 하네여 ㅎㅎ

그리고 이런 하나의 튜플을 만들기 위해서는 아마, 2 + 3 을 먼저 입력하고 이걸 그대로 복붙해서 문자열로 옮기는 과정을 거칠텐데요, 이 과정에서는 어떤 문제점이 있을까요?

복-붙

우선 숫자 -> 문자열로 복붙하는 과정은 반복적입니다. 한마디로 귀찮습니다. 또한 숫자를 문자열로 옮기는 과정에서 오타가 발생할 수도 있기 때문에, Int 로 계산 된 결과가 String 으로 표현되는 식의 결과와 일치하는지도 보장할 수 없습니다.

🤔 : 그러면 2 + 3 이라는 표현식만 넣으면, 자동으로 결과값과 식을 튜플로 만들어주면 좋겠음!

결과값과_식을_만들어라(2+3) // (2 + 3, "2 + 3")

이를 위해 우리는 해당 기능을 하는 매크로를 #stringify 라는 이름으로 정의하고, 아래와 같이 사용할 수 있습니다.

이중 하나를 우클릭해서 Expand Macro 로 펼쳐보면

정확히 우리가 원하던 형태로 매크로가 확장된 것을 알 수 있습니다.

🤔 : ????? #stringify 가 어디 있는건데????

ㅎㅎ.. 그럼 이 #stringify 라는 매크로를 만들러가볼까요?

아까 설명했듯, Swift 매크로는 다른 라이브러리들과 비슷하게 패키지 형태로 배포 됩니다. 그렇기 때문에 File > New > Package 를 클릭하고

Swift Macro 를 클릭해줍니다 (참고로 Xcode 14.3 에서는 동일한 동작을 하면, 이런 옵션이 뜨지 않지 않습니다)

이 매크로 패키지의 이름은 KWDC 라고 해보겠습니다

그러면 아래와 같은 구조가 생겨 있는데요,

하나씩 짚고 넘어가기 전에 Package.swift 문법이 익숙하지 않을 분들 위해 이곳 먼저 살짝 보고 넘어가보겠습니다.

먼저 targets 으로는 각 Target 을 명시하는데요, 각 타겟에는 Swift Package Manager가 모듈 또는 test suite 로 컴파일하는 소스 파일들이 포함되어 있습니다. 이곳에는 macro, target, excutableTarget, testTarget 네 개가 정의되어있네요!

실제 프로젝트 구조도 이곳에 명시된 각각의 모듈로 구성된 것을 확인할 수 있습니다.

그리고 외부 package 에서 사용 가능한 target 들은 따로 products 에 명시해둘 수 있습니다.

그러면 나중에 이 KWDC 패키지를 추가할때 선택할 수 있는 product 가 아래와 같이 두 개가 뜨게 되는거죠!

위에서 Package.swift 에 대해 간략히 살펴보았으니, 그럼 본격적으로 먼저 KWDC 모듈을 살펴보겠습니다.

Package.swift 주석에도 써있듯, 해당 모듈은 client program 에게 API 의 일부로서 매크로를 노출한다고 합니다.

// Library that exposes a macro as part of its API, which is used in client programs.
.target(name: "KWDC", dependencies: ["KWDCMacros"]),

이 안에는 [Macro name].swift 형식으로 되어있는 KWDC.swift 파일이 있습니다.

이곳에 매크로를 선언(declaration)할 수 있습니다.

앗, 근데 stringify.. 어디서 많이 보지 않으셨나요? 바로 아까부터 계속 예시로 들었던 매크로의 예시와 똑같습니다!

사실 stringify 매크로는 Swift Macro 를 생성할때 Xcode의 템플릿에 포함된 매크로입니다.

그럼 stringify 의 선언 부분을 다시 살펴보도록할까요?

= 왼쪽만 보면 뭔가 구현부는 없는게 protocol 에 선언된 함수 같기도 한데요, = 오른쪽 기준으로는 구현부가 명시되어있습니다.

사실 이곳에는 아래와 같이 다른 매크로가 이곳에 올수도 있지만,

대개는 #externalMacro 로 표현되는 외부 매크로를 사용합니다. #externalMacro 키워드를 통해 별도의 플러그인에서 매크로를 확장하도록 관계를 정의할 수 있습니다.

#externalMacro(module: "KWDCMacros", type: "StringifyMacro")

#externalMacro선언(declaration)과 이를 구현하는 타입 사이의 링크를 생성합니다.

이 안에 명시된 모듈과 타입은, 컴파일러가 매크로 확장 작업을 수행하기 위해 KWDCMacros 모듈의 StringifyMacro 타입을 살펴봐야 한다는 것을 알려줍니다.

그럼 KWDCMacros 모듈로 가보겠습니다.

아까 Package.swift 에서는 .macro 로 정의되어 있었는데요, 주석에 따르면 macro 의 source 변환을 수행하는 구현이 들어있다고 합니다.

// Macro implementation that performs the source transformation of a macro.
.macro(
name: "KWDCMacros",
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax")
]
),

이 안에는 [Macro name]Macro.swift 형식으로 되어있는 KWDCMacro.swift 파일이 있습니다.

이곳에서는 매크로의 실제 구현을 작성합니다.

그런데 뭐 ExpressionMacro 를 conform 한다든지, argument 로 FreeStandingMacroExpansionSyntax 를 받는다든지,, 딱 봐도 생소한 표현들이 많아보이니까 일단은 스크롤을 내려보져 ◠‿◠

같은 파일에, 매크로의 구현 바로 아래에는 KWDCPlugin 이라는 CompilerPlugin protocol 을 conform 하는 타입이 정의되어있습니다. 그리고 해당 protocol 이 요구하는 providingMacros 에는 외부에 제공할 Macro 의 type 을 넣어주면 됩니다. 여기서는 바로 위에서 정의한 StrnigfyMacro.self 를 넣어주면 되겠죠?

이렇게 각 매크로자체적인 컴파일러 플러그인 내에서 구현을 정의해둡니다.

그럼 이제 마지막으로KWDCClient 모듈로 가보겠습니다.

Package.swift 주석에도 써있듯, 여기에서는 library 의 client 로서 macro 를 사용할 수 있습니다.

// A client of the library, which is able to use the macro in its own code.
.executableTarget(name: "KWDCClient", dependencies: ["KWDC"]),

이 안에는 main.swift 파일이 있고

이곳에서 매크로의 동작을 테스트해볼 수 있습니다.

아래와 같이 client program 에게 사용될 매크로를 API 형태로 노출하는 역할을 하는 KWDC 모듈을 import 한 후에, #stringify 매크로를 사용해보겠습니다.

이제 타겟을 KWDCClient 로 놓고, run 해보면

프린트 되는 결과는 다음과 같습니다.

또 다른 예시로, 17 + 25 를 stringify 의 인자로 바로 할당하는 대신, a 와 b 라는 변수에 먼저 할당 후 인자로 넣어보겠습니다.

그러면 결과값은 42 로 똑같지만, 우리가 넘긴 표현식 a + b 가 그대로 찍히는 것을 알 수 있습니다.

stringify 라는 이름 값.. 제대로 하고 있는 것 같져?

동작 방식

우리는 지금까지 매크로가 정의된 패키지의 구조를 살펴 보고, 직접 매크로를 사용해보았습니다. 그렇다면 이 매크로는 과연 어떤 플로우를 거쳐서 우리에게 (a + b, "a + b") 라는 결과를 보내줄 수 있던걸까요?

지금부터 이 과정을 알아보겠습니다 ㄱㄱ

우선 Swift 컴파일러가 매크로의 사용을 감지합니다.

컴파일러가 stringify 라는 매크로가 존재한다는 것을 알 수 있는 이유매크로 선언(declaration) 이 존재하기 때문입니다.

아까 KWDC 모듈에서 매크로의 선언이 되어있던 것 기억하시나요? 이것이 바로 매크로의 API 를 제공하는 역할을 합니다.

그리고 컴파일러는 전체 매크로 표현식의 소스 코드를, 특수한 컴파일러 플러그인으로 전송합니다.

컴파일러 플러그인에서는 매크로에 대응하는 확장을 수행하기 위해, 실제 매크로의 구현이 정의되어있다고 했져?

따라서 이제 플러그인은 매크로를 처리하고, 이에 의해 생성된 새로운 코드 조각, 즉 확장(expansion) 을 반환합니다.

그런 다음 Swift 컴파일러는 해당 확장을 프로그램에 추가하고, 코드와 확장을 함께 컴파일합니다. 따라서 프로그램을 실행할 때 매크로를 호출하는 대신 직접 확장을 작성한 것처럼 동작합니다.

매크로의 Role

자, stringify 의 선언을 다시 봅시다.

😎 : ㅇㅋㅇㅋ 얘는 매크로고, 이름은 strinfigy 이며, Generic Type 의 인자를 하나 받고, (T, String) 타입의 튜플을 반환하는 애군. 실제 구현은 KWDCMacros 라는 외부 플러그인의 StringfyMacro 타입에 정의되어있겠다!

라고 이제 생각할 수 있나여? 아니라면 유감!

참고로 매크로는 항상 public으로 선언됩니다. 매크로를 사용하는 코드와 해당 매크로를 정의하는 코드다른 모듈에 위치하기 때문입니다.

그런데 잠깐.. 여기서 저희가 흐린눈을 해온 키워드가 하나 있습니다. 바로 @freestanding(expression) 죠!

이렇게 매크로에 지정된 속성을 매크로 “역할(role)” 이라고 합니다.

역할(role)은 매크로에 대한 규칙인데요, 이는 소스 코드에서 해당 매크로를 호출할 수 있는 위치와 매크로가 생성할 수 있는 코드의 종류를 결정합니다.

구체적으로 이 역할을 통해서 매크로가

  1. 어디에 적용되는지 (Where it can be used)
  2. 어느 타입의 코드에 확장되는지 (What types of code it expands into)
  3. 그 확장이 코드의 어느 곳에 삽입되는지 (Where the expansions are inserted)

를 결정합니다.

그러면 우리는 이 매크로 역할(role)을 보고 “음 매크로의 확장이 어떤 식으로 이뤄지겠군~~” 하고 예측 할 수 있습니다.

모든 매크로는 하나 이상의 역할을 가지고 있는데요, 이러한 역할을 생각하지 않고 매크로를 작성하는 것은 불가능합니다. 즉, 매크로를 작성하기 전에 우리가 작성할 매크로가 어떤 역할을 할거냐! 부터 정의할 수 있어야합니다.

매크로는 크게 두 종류로 나뉩니다. 바로 freestanding 매크로와, attached 매크로인데요! 그리고 이 안에서 각각의 역할이 세분화 되어 나뉩니다. 하나씩 알아보도록 할까요?

🤔 : 아 잠깐 잠깐;;; freestanding 매크로랑 attached 매크로가 뭔데;;

ㅎㅎ.. 아까 설명 초반에 매크로에는 두 종류가 있고, 아래와 같은 특성을 가지고 있다고 했습니다.

  1. freestanding 매크로 — 항상 샵(#) 기호로 시작
  2. attached 매크로 — 항상 at(@) 기호로 시작

조금 더 자세히 알아볼까요?

우선 freestanding 매크로는 이름 그대로 독립적으로 사용됩니다. 어떠한 선언과도 관련이 없습니다.

예를 들어 아래와 같은 코드가 있다고 할 때, 여기서 #someUnknwonMacro 는 주위에 정의된 어떤 타입이나, 함수 등에 전혀 관계 없이 그 자체로 동작 합니다.

다음으로 attached 매크로는, 이름에서 유추할 수 있듯, 어떠한 타입이나 함수, 변수 등의 선언과 연관되어 동작합니다.

이제 여기까지

🤔 : freestanding 매크로? 독립적으로 혼자 동작하는거! 사용하는 쪽에서 # 로 시작하면 독립형 매크로다 ㅇㅇ / attached 매크로? 어떤 선언과 연관되어 동작하는거! 사용하는 쪽에서 @ 기호로 시작하면 attaced 매크로다 ㅇㅇ

를 이해했으므로, 원래 알아보려고 했던 macro 의 role 로 돌아와보겠습니다.

매크로의 종류 안에서 각각의 역할이 세분화 되어 나뉜다고 했는데요, freestanding 매크로에는 2개의 role 이, attached 매크로에는 5개의 role 이 있네요!

  • @freestanding(expression) - 값을 반환하는 코드 조각을 생성합니다
  • @freestanding(declaration) - 하나 이상의 선언(declaration)을 생성합니다
  • @attached(peer) - 적용된 선언과 함께 새로운 선언을 추가합니다
  • @attached(accessor) - 프로퍼티에 접근자(accessor)를 추가합니다
  • @attached(memberAttribute) - 적용된 타입 또는 익스텐션의 선언에 attribute 를 추가합니다
  • @attached(memeber) - 적용된 타입 또는 익스텐션 내부에 새로운 선언을 추가합니다
  • @attached(extension) - 타입 또는 익스텐션에 프로토콜 적합성을 추가합니다

그럼 한개씩 알아보러 가볼까요?

freestanding(expression) — 값을 반환하는 코드 조각을 생성

먼저 freestanding(expression) 역할로 시작해 보겠습니다.

expression 은 실행(excute)되고 결과를 생성하는 코드의 단위를 말합니다.

아래에서 등호 뒤의 산술식이 바로 expression 입니다.

expression은 재귀적인 구조를 가지고 있기 때문에, 종종 더 작은 expression 으로 구성될 수 있는데요, 따라서 x + width 자체도 expression이고, width라는 단어 자체도 expression입니다.

freestanding(expression) 매크로는 이러한 expression 으로 확장되는 매크로를 뜻합니다.

🤔 : 어떤 케이스에서 사용함?

아래의 코드를 보겠습니다.

guard let 구문으로 옵셔널을 언래핑하고, else 에서는 failure 함수를 호출하는게,, 굉장히 익숙하죠?

하지만 언래핑을 할 옵셔널 값이 많아진다면, 이 옵셔널을 해제하고 예외 처리를 하는 과정이 굉장히 반복적이고 귀찮게 느껴질 수 있습니다.

🤔 : optional 값과 에러 메시지만 넘기면, 자동으로 optional 값이 있을때 unwrapping 해서 빼내고, 없을때 failure 처리를 할 수 없을까?

라고 생각할 수 있습니다. 이렇게 말이죠!

let image = #unwrap(downloadImage, message: "was alrday checked")

이때, 이 매크로의 역할은 값을 계산하고 반환해야하므로 freestanding(expression) 역할로 만듭니다

구현부는 일단 생각하지 않기로 하고, 이제 우리는 함수처럼 호출할 수 있는 매크로를 얻게 되었습니다.

이 매크로는 아래와 같이 guard let 을 포함한 expression 으로 확장됩니다. 심지어 오류 메시지에는 일반 함수로는 불가능한 변수 이름도 포함할 수 있습니다.

freestanding(declaration) — 하나 이상의 선언을 생성

다음으로 freestanding(declaration) 역할을 살펴보겠습니다. 이 역할은 독립적으로 함수, 변수 또는 타입과 같은 하나 이상의 선언으로 확장됩니다.

어떤 용도로 사용할 수 있을까요? 아래와 같은 코드가 있다고 가정해보겠습니다.

🤔 : ????????????

ㅋㅎ.. 코드를 사실 이해할 필요는 없고 그냥 이차원 배열을 아래와 같이 평면적으로 저장해서 개발자가 (1,1) 과 같은 2차원 인덱스를 전달하면 5 이라는 1차원 인덱스를 반환하는 역할을 한다고 생각해보겠습니다.

앗 그런데 이제 3차원 배열에 대해 동일한 기능을 제공하고 싶어졌네여???

이를 위해 Array3D struct 를 코드로 구현하면, 기존 Array2D struct 와 굉장히 유사한 선언이 만들어집니다. 아래에서 하이라이팅 된 곳 정도가 Array2D 와 달라진 부분입니다.

그러다 4차원 배열과 5차원 배열에 대해서도 동일한 기능이 필요해집니다.

자연스럽게 아래와 같은 생각이 들게되죠

🤔 : 그냥 몇차원 배열인지에 대한 정보만 넘기면, struct 구현체 생성해주면 안돼?;;;

다행히 각 구조체는 선언, 즉 declaration 이므로 freestanding(declaration) 매크로를 통해 생성할 수 있습니다.

makeArrayND 라는 이름으로 선언된 이 매크로는 차원의 수를 Int 로 전달할 뿐, 따로 return 하는 값은 없습니다. 왜냐하면 이 매크로는 결과를 계산하는 것이 아니라 프로그램에 선언(declaration)을 추가할 것이기 때문입니다.

이제 각 호출은, 전달된 크기에 대한 알맞은 계산이 포함된 다차원 배열 타입으로 확장됩니다.

앗 그런데 잠깐,, 아까 declaration role 을 추가할때 뭔가 이상함을 느끼지 않으셨나여? 저 뒤에 names 로 붙어있는 값은 뭘까요??

간단히 말해 매크로를 통해 생성될 멤버의 이름을 지정해준다고 이해하면 되는데요, 지금 보고 있는 declaration 매크로와, 이후에 볼 peer 매크로, member 매크로에서 이 names 를 지정해줄 수 있습니다.

사용할 수 있는 이름 지정자(name specifier)는 아래 다섯 가지가 있습니다. 이에 대한 더 자세한 내용은 [WWDC23] Expand on Swift macros 세션과 Attached Macros proposal 문서 등을 참고해주세요

  • overloaded: (attahced only) 매크로가 부착된 선언과 동일한 기본 이름을 가지는 선언을 생성합니다.
  • prefixed (<some prefix>): (attahced only) 매크로가 부착된 선언의 기본 이름 뒤에 <some prefix> 가 오는 선언을 생성합니다. <some prefix>$로 시작할 수 있습니다.
  • suffixed (<some suffix>): (attahced only) 매크로가 부착된 선언의 기본 이름 뒤에 <some suffix> 가 오는 선언을 생성합니다.
  • named (<some name>): 기본 이름이 <some name> 인 선언을 생성합니다.
  • arbitrary: 위의 규칙들로 이름을 설명할 수 없는 선언을 생성합니다. 해당 specifier 를 사용하는 것은 매우 일반적입니다.

지금까지 우리는 freestanding 매크로에 속하는 역할에 대해서 살펴보았습니다. 이제 attached 매크로로 넘어가보겠습니다.

attached 매크로는 특정 선언에 연결되어 있으므로 더 많은 정보를 활용할 수 있습니다. freestanding 매크로는 전달된 argument만을 사용하지만, attached 매크로는 연결된 선언에 대한 접근도 가능합니다. 이 매크로는 종종 해당 선언을 검사하고 이름, 타입 및 기타 정보를 추출합니다.

뭔 말인지 모르겠으니까 이 매크로에 속한 역할을 하나씩 살펴보도록 할까요?

attached(peer) — 새로운 선언을 추가

@attached(peer) 역할로 시작하겠습니다. peer 매크로는 변수, 함수, 타입뿐만 아니라 import 및 연산자 선언(operator declarations)과 같은 어떤 선언에든 붙어서, 그 옆에 새로운 선언을 삽입할 수 있습니다. 따라서 메서드나 프로퍼티에 사용하면 얘네가 속한 타입의 멤버를 생성하게 되지만, 최상위 함수나 타입에 사용하면 새로운 최상위 선언을 생성하게 됩니다.

🤔 : 예 시 내 놔

아래와 같이 async 함수가 구현 되어있지만, 오래된 버전 또한 지원하기 위해서 completion handler 를 사용하는 API 를 제공하려고 합니다.

이러한 메서드를 작성하는 것은 어렵지 않습니다. async 키워드를 제거하고, completion handler 파라미터를 추가하고, result 타입을 parameter list 로 이동시키고, aysnc 함수를 Detached Task 에서 호출하면 됩니다. 아래처럼요!

하지만 이런 함수가 많다고 가정해보면,,,,,,,,, 벌써 귀찮죠?

그냥 아래와 같이, 기존 구현되어있는 async 함수에 completion handler 파라미터에 해당하는 이름만 넘겨주면, 이에 대응하는 예전 버전의 API 를 제공해주면 좋겠다는 생각이 듭니다.

바로 이런 경우에는 attached(peer) 매크로가 적합합니다.

이렇게 선언한 매크로를 메서드의 async 버전에 첨부하면?!

아래와 같이 completion handler 기반의 시그니처를 생성하고, 메서드 body 를 작성하는 식으로 확장됩니다. 짱이져??

attached(accessor) — 프로퍼티에 접근자를 추가

다음으로 attached(accessor) 에 대해 알아보겠습니다. 이 역할은 변수나 서브스크립트에 붙어서 get, set, willSet, didSet 과 같은 접근자(accessor)를 추가할 수 있습니다.

이런 기능이 도대체 왜 유용하다는 걸까요? 예시를 보도록 하져. 참고로 이 예시는 나머지 role 들에 대한 설명으로까지 계속 이어지니까 집중 한번 하고 가겠습니다.

Person 구조체에서는 name, height, birthDate 라는 프로퍼티를 정의하고 각각의 getter 와 setter를 통해 dictionary 에 있는 값에 접근하고 있습니다.

이렇게 하면 손쉽게 dictionary 의 name, height , birth_date 필드에 접근할 수 있으면서도, dictionary 에 있는 이외의 다른 정보들은 그 정보를 그대로 유지하되 무시할 수 있습니다.

하지만 딱 봐도 각각 getter 와 setter 를 수동으로 작성하는게 귀찮아보이죠?

🤔 : 오? get set? 뭔가.. property wrapper 를 사용해 볼 수도 있지 않을까???

싶기도 하지만, property wrapper 는 함께 사용되는 타입의 다른 stored property 에 접근할 수 없기 때문에 부적절합니다.

그러므로 이를 돕기 위한 attached(accessor) 매크로를 작성해보겠습니다.

이제 각 속성 앞에 @DictionaryStorage 를 넣으면

매크로가 아래와 같이 접근자를 생성해줍니다.

오~~ 좋은데??? 싶지만 여전히 일부 boilerplate 가 남아있습니다

바로 동일한 DictionaryStorage attribute 입니다. 이 attribute 도 한곳에서 한번에 붙여줄 수는 없을까요?

attached(memberAttribute) — 타입 또는 익스텐션의 선언에 attribute 를 추가

이를 위해 attached(memberAttribute) 를 사용할 수 있습니다. 이 매크로는 type 또는 extension 에 붙어서, 이곳의 모든 멤버에 attribute 를 추가할 수 있습니다.

이미 있는 DictionaryStorage 매크로에 @attached(accessor) role 과 함께 또 다른 role attribute 를 추가하겠습니다.

🤔 : ??? 엥 하나의 매크로가 여러 역할을 할 수 있는거임??

이라고 생각하신다면,, 반은 맞고 반은 틀립니다! attached role 들은 조합할 수는 있지만, 앞에서 살펴본 두 개의 freestanding role 은 조합할 수 없습니다.

Swift는 적용한 모든 role 을 맥락에 맞게 확장합니다. 예를 들어 아래와 같은 role 을 가진 DictionaryStorage

  • 타입에 첨부하면 @attached(memberAttribute) role 을 확장하고
  • 프로퍼티에 첨부하면 @attached(accessor) role 을 확장하고
  • 함수에 첨부하면 DictionaryStorage 는 함수에 첨부할 수 있는 role 이 없으므로 컴파일 오류가 발생합니다

어쨌거나 DictionaryStorageattached(memberAttribute) 라는 두 번째 role 을 추가하면, 각 property 에 개별적으로 attribute 를 붙이는 대신, 타입 자체에 붙여서 이곳의 모든 멤버에 attribute 를 추가할 수 있습니다.

이 매크로의 구현체에는 initializer 나 dictionary 프로퍼티, 혹은 이미 @DictionaryStorage attribute 가 있는 birth_date 와 같은 특정 멤버를 건너뛰는 로직이 있을 것입니다. 하지만 다른 stored property 에는 DictionaryStorage attribute 를 추가하고

이렇게 적용된 attribute 들은 우리가 아까 봤던 @attached(accessor) 매크로에 의해 다시 확장될 것입니다.

attached(memeber) — 타입 또는 익스텐션 내부에 새로운 선언을 추가

하지만 여전히 제거할 수 있는 boilerplate 가 있습니다.

바로 DictionaryRepresentable 프로토콜에서 요구되는 initializer 와 stored property 입니다.

이를 위한 매크로를 작성하기 위해 @attached(member) role 을 적용할 수 있습니다.

@attached(memberAttribut) 매크로와 마찬가지로, 이 매크로를 타입과 익스텐션에 적용할 수 있지만, 기존 멤버에 attribute 를 추가하는 대신 완전히 새로운 멤버를 추가합니다. 따라서 method, property, 이니셜라이저 등을 추가할 수 있습니다. 심지어 클래스와 구조체에 stored property 나, enum 에 case를 추가할 수도 있습니다.

여기서 다시 한 번, DictionaryStorage 매크로에 새로운 @attached(member) role 을 추가해서 다른 role 들과 함께 조합할겁니다!

매크로에서 이 role 을 실제로 구현할 때는 이니셜라이저와 dictionary 라는 property 를 추가하고 있겠죠??

DictionaryStorage에 @attached(member) role이 추가된 지금 상태에서는 initialzer, dictioanry 두 멤버를 직접 작성할 필요가 없어졌습니다.

이제 타입에 DictionaryStorage 를 사용하기만 하면 자동으로 initialzer, dictioanry 가 추가되는 식으로 구현이 되어있기 때문이져

attached(extension) — 타입 또는 익스텐션에 프로토콜 conformance 및 멤버 list 추가

하지만 아래 코드에서도, 아직 제거해야 할 보일러플레이트가 하나 남아있습니다. 바로 DictionaryRepresentable 프로토콜에 대한 conformance 입니다.

이런 경우에는 마지막으로 살펴볼 @attached(extension) role 이 완벽합니다. 이 role 은 타입이나 익스텐션에 conformance를 추가할 수 있습니다.

앗 근데 뭔가 이상하지 않나여? 설명은@attached(extension) 라고 해놓고, 왜 첨부된 이미지는 @attached(conformance) 일까요?

정답은 바로 WWDC 에서 발표한 시점에는 conformance 였지만, 아래 논의를 통해 extension 으로 이름이 바뀌었기 때문입니다 ㅋ

그래도 대체되었다고 알려는 주는 애플…

이는 conformance매크로 역할을 extension매크로 역할로 일반화하는 것을 제안하는데요, 이에 따라 extension에 프로토콜과 where 절 뿐만 아니라 멤버 목록(member list)을 추가할 수 있는 기능을 제공합니다.

This proposal generalizes the conformance macro role as an extension macro role that can add a member list to an extension in addition to a protocol and where clause.

튼 이렇게 해당 role 을 추가하고나면, 이제 conformance를 수동으로 작성할 필요가 없어졌습니다.

왜냐하면 이제 DictionaryStorage attribute 를 통해 이미 수행하던 다른 모든 작업과 함께 protocol conformance 또한 자동으로 추가하기 때문이져

그런데 여기서 잠깐.. 이렇게 여러개의 role 을 가진 매크로에서는 어떤 role 이 먼저 확장되는지 궁금할 수 있습니다.

답은 “그건 중요하지 않다!” 는 건데요! 각각은 다른 매크로에 의해 확장되기 이전의 원래 버전의 선언을 볼 것입니다. 따라서 순서에 대해 걱정할 필요가 없습니다. 컴파일러가 매크로를 언제 확장하더라도 동일한 결과를 볼 수 있습니다.

휴! 멀리도 왔네요!! 그럼 이 Person struct 가 원래는 어떻게 생겼었는지 다시 한번 보겠습니다.

여기서 우리는 대부분의 코드를 매크로의 여러 role로 이동시켰고요,

결국 이 특정 타입에 대한 특수한 내용만을 간결하게 지정할 수 있도록 만들었습니다.

DictionaryStorage 를 사용할 수 있는 타입이 많아지면 많아질수록 더 편해지겠져?

매크로 구현

이쯤 오니

🤔 : 으응.. 그래..! 매크로 좋아..! 최고네..? 근데.. 내가 이런 확장되는 코드를.. 어떻게 구현 할 수 있는걸까..?

라는 생각이 들게 됩니다.

이를 이해하기 위해 우리가 위에서 쭉 설명해온 DictionaryStorage 매크로가 어떻게 구현될 수 있는지 살펴보겠습니다.

우선 DictionaryStorage 매크로에는 이 네 가지 role 이 있었습니다.

각각의 role 에는 해당하는 프로토콜이 있으며, 실제 구현은 제공하는 각 role 에 대한 프로토콜을 준수해야합니다.

따라서 DictionaryStorageMacro 타입은 해당하는 네가지 프로토콜을 준수해야합니다.

하지만 매크로 구현이라는 핵심에 집중하기 위해, 지금은 MemberMacro 준수에만 집중해보겠습니다. 이 프로토콜은 @attached(member) role 이 요구하는 protocol 입니다.

참고로 @attached(member) role 은 type 이나 extension 에 member 를 추가하는 역할을 하는데요, DictionaryStroage 예제에서는 initializer 와 dictionary 라는 stored property 를 추가하고 있었져?

그럼 매크로의 구현부로 와보겠습니다. MemberMacro 프로토콜은 expansion 이라는 단일 요구 사항을 구현해야합니다.

실제로 구현을 시작하기 전에, 매크로가 예상대로 작동하는지 확인하기 위해 테스트 주도 방식으로 개발을해보겠습니다. 매크로 플러그인은 일반적인 Swift 모듈이므로 일반적인 단위 테스트를 작성할 수 있고, TDD 방식은 매크로 개발시 매우 권장되는 접근 방식입니다.

따라서 테스트 케이스를 작성하기 전까지는 구현을 비워둘 것입니다. 매크로의 동작을 테스트 케이스에서 정의한 후, 해당 테스트 케이스와 일치하도록 구현을 작성할 것입니다. 따라서 실제 구현부는 우선 빈 배열을 반환하기로 합시다.

그리고 DictionaryStorageMacro 를 컴파일러에게 노출 시키기 위해 providingMacros 속성에 추가합니다.

이제 테스트코드를 작성하러가보겠습니다. assertMacroExpansion 함수를 사용하여 매크로의 동작을 확인할거예요!

테스트하려는 것은 Person struct 에 적용되었을 때 매크로가 생성하는 내용이므로 이를 테스트 케이스의 입력으로 사용합니다.

지금은 매크로가 아직 아무 작업을 하지 않으므로, attribute 를 제거하고 새로운 멤버를 추가하지 않을 것입니다. 따라서 예상되는 확장 코드는 입력과 동일하지만 @DictionaryStorage 는 없는 것입니다.

마지막으로, 테스트 케이스에게 DictionaryStorageMacro 구현을 사용하여 DictionaryStorage 매크로를 확장해야 한다는 사실을 알려야합니다. 이를 위해 매크로 이름과 type 의 구현을 딕셔너리에 매핑해서 assertion 함수의 마지막 인자로 전달합니다.

이제 작성한 테스트를 실행하여 지금까지 작성한 내용이 실제로 작동하는지 확인해봅시다.

잘 실행되네요!!

하지만 우리가 실제로 원하는 것은 매크로가 속성을 제거하는 것이 아니라 이니셜라이저 및 프로퍼티를 생성하는 것을 확인하는 것입니다. 따라서 매크로 플러그인이 생성하기를 원하는 코드를 다시 결과값으로 넣어보겠습니다.

테스트를 다시 실행하면…?

당연히 실패합니다 ㅋ 왜냐하면 우리의 매크로는 아직 빈 배열만 반환하고 있기 때문이져??? 따라서 아래와 같이 구현부를 채워보겠습니다.

그러면 성공해버렸죠? ㅋ

그럼 다시 구현부로 돌아와 하나씩 살펴보도록 하겠습니다.

실제 매크로의 구현은 SwiftSyntax 라는 라이브러리를 가져오는 것으로 시작합니다.

SwiftSyntax는 소스 코드를 특별한 트리 구조로 표현합니다.

예를 들어, 이 코드 샘플의 Person 구조체는 StructDeclSyntax 라는 타입의 인스턴스로 표현됩니다.

하지만 이 StructDeclSyntax 는 더 잘게 쪼개질 수 있습니다. 소스 코드에서 @DictionaryStorageAttributeListSyntax 로, struct 키워드는 TokenSyntax.keword 로, Person 은 TokenSyntax.indentifier로, 블록 {} 안의 정보들은 MemberDeclBlockSyntax 로 다시 나눠지게 되죠.

그리고 이런 식으로 syntax tree 를 파고들면, 소스 파일이 결국에는 토큰 노드들로 커버된다는 것을 알 수 있습니다. 토큰은 이름, 키워드, 구두점과 같은 소스 파일의 특정 텍스트 부분을 나타내며, 해당 텍스트와 공백 및 주석과 같은 주변의 사소한 정보를 포함합니다.

이렇게 매크로의 동작은 소스 코드를 Swift Syntax 트리로 파싱하는 걸로 시작합니다. 그리고 SwiftSyntax 가 이 작업을 도와주는거져!

SwiftSyntax 라이브러리 외에도 두 개의 모듈을 추가로 가져옵니다. 하나는 SwiftSyntaxMacros 로, 매크로 작성에 필요한 프로토콜과 타입을 제공합니다.

다른 하나는 SwiftSyntaxBuilder입니다. 이 라이브러리는 구문 트리를 구성하기 위한 편리한 API를 제공합니다. 이 라이브러리 없이도 매크로를 작성할 수는 있지만, 사용하는게 훨씬 편리하므로 import 하겠습니다.

이제 모든 준비는 완료되었으니 실제로 플러그인이 제공해야 하는 DictionaryStorageMacro 타입을 작성해보겠습니다.

아까도 말했듯 expansion(of:, providingMembersOf, in:) 메서드는 MemberMacro protocol 이 요구하는 메서드입니다. 이때 주목해야할 점은 해당 메서드가 정적 메서드(static method)인건데요! 모든 확장 메서드는 정적 메서드이므로, Swift는 실제로 DictionaryStorageMacro 타입의 인스턴스를 생성하지 않습니다. 그저 메서드를 담는 컨테이너로 사용합니다.

각각의 확장 메서드는 소스 코드에 삽입될 SwiftSyntax 노드를 반환합니다. MemberMacro 는 타입에 추가할 선언들의 목록으로 확장되므로 DeclSyntax 노드의 배열을 반환합니다.

body 에는 매크로가 추가하길 원하는 이니셜라이저와 property 가 배열로 포함되어 있습니다.

이곳에 있는 var dictionary 부분은 일반적인 문자열인 것처럼 보이지만, 실제로는 그렇지 않습니다.

이 문자열 리터럴은 DeclSyntax 타입이 예상되는 위치에 작성되어 있으므로, Swift는 실제로 이를 소스 코드의 일부로 처리하고 Swift 파서에게 DeclSyntax 노드로 변환하도록 요청합니다. 이는 아까 import 한 SwiftSyntaxBuilder 라이브러리가 제공하는 편의 기능 중 하나입니다.

만약 그렇지 않다면 직접 아래처럼 syntax node 를 구성한다든가.. 해야할텐데여 ㅋ

Syntax Tree 에서 생성할 syntax node 를 구성하는 작업은 다루는 범위가 너무 크기 때문에 참고할 만한 몇 가지 소스를 말씀드리겠습니다.

  1. 하나는 WWDC23 의 Write Swift Macros 세션인데, 이 세션에서는 특정 소스 코드가 구문 트리로 어떻게 표현되는지 이해하는 데 도움이 되는 실용적인 팁을 제공합니다.
  2. 다른 하나는 SwiftSyntax 패키지의 문서를 살펴보는 것입니다.
  3. 마지막으로 애플에서 소개하지는 않았지만 Swift AST Explorer 페이지에서는 소스 코드가 어떻게 SwiftSyntax 로 표현되는지 정보를 제공합니다.

에러 처리

여기까지 다 좋습니다. 그런데 갑자기 이 매크로를 구조체(struct)가 아닌 열거형(enum)에 적용하려고 한다면 어떻게 될까요?

enum 은 저장 프로퍼티를 가질 수 없으므로, Swift는 Enum 은 stored properties 를 포함할 수 없습니다라는 에러를 발생시킵니다.

하지만, 애초에 @DictionaryStorage를 enum 에 적용을 할때부터 DictionaryStoarge 는 struct 에만 적용할 수 있습니다 라는 명확한 에러를 발생시키면 더 좋을 것 같습니다.

이를 위해 지금까지 무시한 expansion 메서드의 파라미터들을 살펴보겠습니다. 각 role 마다 argument 는 약간씩 다르지만, MemberMacro 의 경우 세 가지가 있습니다.

먼저 살펴볼건 두 번째 파라미터인 declaration 인데여, DeclGroupSyntax 를 conform 하는 타입입니다.

DeclGroupSyntax 는 stuct, enum, class, actor, protocol 및 extension 을 위한 노드가 모두 준수하는 프로토콜입니다.

뭔말이냐면 만약 매크로를 struct 에 적용했다면 여기에는 StructDeclSyntax 가 들어오고,

매크로를 enum 에 적용했다면 EnumDeclSyntax 가 들어온다는 뜻이져

따라서 먼저 문제를 감지하기 위해 declaration 파라미터가 StructDeclSyntax 인지 체크하고, 선언이 struct가 아니라면 else 블록에서 빈 배열을 반환하여 매크로가 프로젝트에 코드를 추가하지 않도록 하겠습니다.

하지만 실제로 원하는 것은 에러를 발생시키는 건데요, 제일 간단한 방법으로 일반적인 Swift 에러를 throw 할 수도 있습니다.

그럼 사용하는 쪽에서는 이런식의 에러 가 뜨게 되죠

하지만 더 정교한 오류를 생성하기 위해 Diagnostic 타입의 인스턴스를 사용할 수 있습니다.

Diagnostic 의 뜻은 "진단" 인데요

컴파일러나 매크로는 오류나 경고를 syntax tree 를 살펴보며 진단하기 때문에, 오류를 나타내는 인스턴스를 Diagnostic 이라고 부릅니다.

Diagnostic 를 사용하려면 우선 import SwiftDiagnostics 를 합니다.

그리고 Diagnostic instance 생성을 위해서는 최소한 두 가지 정보가 필요합니다.

첫 번째로 필요한 정보는, 오류가 발생한 syntax node 입니다. 이를 통해 컴파일러는 어느 줄을 잘못된 것으로 표시할지 알 수 있습니다. 우리는 여기서 사용자가 작성한 @DictionaryStorage attribute 에 에러를 표시하고 싶었져 ㅇㅇ

따라서 expansion 함수의 첫번째 인자인 node 를 (wwdc 세션에서는 parameter name 이 attribute 였지만, Xcode 15 beta 1로 봤을때는 node 로 되어있더라구여 ㅋㅎㅎ)

그대로 넘겨줍니다 (wwdc 에서는 node 를 바로 전달하는데, Xcode 15 베타 1 기준으로 똑같이 따라하면 AttributeSyntax 타입의 값이 Syntax 타입으로 들어갈 수 없다고 에러가 뜨더라구여? 그래서 Syntax(node) 처럼 한번 감싸줬습니다)

그럼 여기서 “🙄 엥? 노드가 뭔데??????” 라는 생각이 들겠져?

여기에는 실제로 개발자가 사용한 매크로가 넘어옵니다. 따라서 실제로 브레이크 포인트를 찍고 po node 를 해보면

아래와 같은 At 사인에 DictionaryStorage Identifier 를 가진 AttributeSyntax 를 확인할 수 있습니다.

Diagnostic 생성을 위해 필요한 두 번째 인자는 컴파일러에 표시할 실제 메시지입니다. 이를 위해 사용자 정의 타입을 생성한 다음 해당 인스턴스를 전달해야 합니다.

여기서는 예시로 사용한 MyLibDiagnostic 타입을 간단히 살펴보겠습니다.

여기서는 case 로 에러나 워닝 케이스를 정의해두고

DiagnosticMessage 프로토콜을 conform 해서 필요한 여러 property 를 정의해야하는데요,

우선 severity 는 진단이 오류인지 경고인지를 지정합니다.

message 는 실제 오류 메시지를 생성합니다.

마지막으로 diagnosticIDdomain에는 플러그인의 모듈 이름을 사용하고 id 에는 고유한 문자열을 사용해야 합니다. 우선 여기서는 편의상 enum 의 raw value를 사용했습니다.

이렇게 인자로 들어갈 노드와 메시지가 모두 준비되면Diagnostic 을 생성할 수 있고, 이를 context.diagnose 인자로 넘김으로써 적절한 진단을 하도록 알릴 수 있습니다.

🙄 : ?????????? 잘나가다가 context 는 또 뭔데;;

ㅎㅎ context 는 expansion 함수의 세번째 파라미터로, 매크로 구현이 컴파일러와 communicate 할 때 사용되는데요! 에러 및 워닝 발생을 포함하여 몇 가지 다른 작업을 수행할 수 있습니다.

이제 실제로 enum 에 적용해보면 우리가 원하는대로 에러가 뜨는 것을 확인 할 수 있습니다.

필요하다면 훨씬 더 정교한 진단을 생성할 수 있는데요, 예를 들어 Xcode 에서 Fix 버튼을 누르면 자동으로 적용되는 수정 사항(Fix-Its)을 diagnostic 에 추가할 수 있습니다.

그러면 이런식으로 더 적절한 오류를 제공할 수 있겠져?

주의 사항

여기까지 진행한 시점에서, 아마도 ‘오~~ 이런 상황에서 매크로 쓰면 좋을 것 같은데?’ 라는 아이디어가 떠오르기도 할 것 같습니다. 그 중에는 ‘날짜랑 시간을 삽입하는 매크로를 작성해 봐야지!!!’ 라고 생각하시는 분도 계실텐데요!

하지만 그러시면 안됩니다,,,

🤔 : 에.,,,,,,,,,? 왜요?! ㅜㅜㅜㅜ

매크로는 컴파일러가 제공하는 정보만을 사용해야 합니다. 컴파일러는 매크로 구현이 순수 함수이며, 제공된 데이터가 변경되지 않았다면 확장도 변경되지 않을 것이라고 가정합니다. 이를 우회하면 일관성 없는 동작이 발생할 수 있습니다.

매크로 시스템은 이러한 규칙을 위반할 수 있는 일부 동작을 방지하기 위해 설계되었는데요, 컴파일러 플러그인은 디스크 파일 읽기나 네트워크 접근과 같은 동작을 막는 샌드박스에서 매크로 구현을 실행합니다. 즉, 플러그인은 안전한 샌드박스 내에서 별도의 프로세스로 실행됩니다.

하지만 샌드박스는 모든 부적절한 동작을 차단하지는 않습니다. 날짜나 난수와 같은 정보를 가져오기 위해 API를 사용하거나, 하나의 확장에서 정보를 저장하고 다른 확장에서 사용하는 전역 변수를 사용할 수 있습니다. 하지만 이러한 동작을 수행하면 매크로가 잘못 동작할 수 있습니다. 따라서 그냥 이런 동작을 하지 마세여!!

마무리

지금까지 Swift 매크로에 대해 많이 배웠는데요, 오늘 다룬 내용을 정리해 보겠습니다.

  • 매크로를 통해 복잡한 코드로 확장되는 기능을 만들 수 있음
  • 일반적으로 다른 API 들과 같이 라이브러리에 선언됨. 하지만 실제 구현은 별도의 플러그인에 되어있으며, 이는 안전한 샌드박스에서 실행
  • 매크로의 Role 은 매크로를 사용할 수 있는 위치를 표현하고, 그 확장이 프로그램의 나머지 부분과 통합되는 방식을 나타냄
  • 매크로를 만들기 위해서는 macro 패키지 템플릿을 사용할 수 있음. 이곳에는 시작하기 좋은 예시stringify 매크로가 포함됨
  • 매크로에 대해 단위 테스트를 작성해서 원하는대로 작동하는지 확인하는 것이 좋음
  • 매크로가 특정 상황에서 적용되지 않을 경우에도 맞춤형 오류 메시지 출력하는 것이 좋음

여기까지가 매크로에 대해 준비한 내용입니다!

매크로의 전체적인 내용을 다룬 것 같지만,, 놀랍게도 흐름상 빠진 내용들도 있으니, 매크로를 작성하실 분들은 꼭 아래 두 세션과 문서를 챙겨보시는걸 추천드립니다.

후,, 길고 험난한 시간이었네요,,,!◠‿◠

과연 매크로를 부수는 시간이었을까요, 매크로에게 부서지는 시간이었을까요? ㅎㅎ 모쪼록 전자에 가까웠다면 좋겠네여!

그럼 20000!

--

--