스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
Swift 매크로 작성하기
Swift 매크로를 사용하여 코드베이스를 더욱 표현력 있고 읽기 쉽게 만드는 방법을 알아보세요. 코드를 함께 작성하면서 매크로가 반복적인 코드 작성을 어떻게 피하게 해 주는지 확인하고, 앱에서 매크로를 사용하는 방법을 살펴봅니다. 매크로의 빌딩 블록을 공유하고 테스트하는 방법을 시연하며, 매크로에서 컴파일 오류를 발생시키는 방법을 알아봅니다.
챕터
- 1:15 - Overview
- 5:10 - Create a macro using Xcode's macro template
- 10:50 - Macro roles
- 11:40 - Write a SlopeSubset macro to define an enum subset
- 20:17 - Inspect the syntax tree structure in the debugger
- 24:35 - Add a macro to an Xcode project
- 27:05 - Emit error messages from a macro
- 30:12 - Generalize SlopeSubset to a generic EnumSubset macro
리소스
관련 비디오
WWDC23
-
다운로드
♪ ♪
반복적인 보일러플레이트 작성을 좋아하는 사람은 아무도 없을 겁니다 그래서 Swift 5.9에 Swift 매크로를 도입했어요 Swift 매크로를 사용하면 컴파일 타임에 반복 코드를 생성할 수 있으므로 앱의 코드베이스를 더욱 표현력 있고 읽기 쉽게 만들 수 있습니다 저는 앨릭스 호펜입니다 이번 시간에는 어떻게 매크로를 작성하는지 보여 드릴게요 먼저 매크로 작동 방식을 간략하게 설명할게요
이후 Xcode로 곧장 넘어가서 매크로를 처음 생성하는 방법을 알아볼 겁니다
Xcode에서 첫 번째 매크로를 살펴본 뒤 매크로를 사용할 수 있는 역할을 더 많이 탐색하면서 현재 작업 중인 앱의 코드베이스를 단순화하고자 매크로를 어떻게 사용했는지 보여 드리겠습니다
마지막으로 매크로가 특정 콘텍스트에서 적용할 수 없을 때 컴파일러에 오류나 경고를 다시 전달하는 방법을 알아보죠
그럼 이제 시작해 봅시다 1학년 학생들이 산수 연습에 사용할 만한 계산 목록입니다 결과는 왼쪽에 정수로 계산은 문자열 리터럴로 투플 오른쪽에 표시됩니다 반복적이고 중복이 있으며 오류가 발생하기 쉽죠 결과와 계산이 일치한다고 보장할 수 없으니까요 다행스럽게도 Swift 5.9를 사용하면 stringify 매크로를 정의해 이를 단순화할 수 있습니다
이 매크로는 Xcode의 템플릿에 포함돼 있기도 합니다 stringify 매크로는 계산만 단일 매개변수로 취합니다 컴파일 타임에 앞서 봤던 투플로 확장되어 계산과 결과가 일치하도록 보장해요 어떻게 작동하는 걸까요? 해당 매크로의 정의부터 살펴보죠
stringify 매크로는 함수와 매우 흡사합니다 정수를 입력 매개변수로 취하고 결과와 정수 계산 내용인 문자열을 포함하는 투플을 출력합니다 매크로 표현식의 인수가 매개변수와 일치하지 않거나 타입 검사를 자체적으로 수행하지 않으면 컴파일러가 매크로 확장을 적용하지 않고 오류를 내보냅니다
예를 들어 문자열 리터럴을 이 매크로에 전달했더니 String을 예상 인수 타입 Int로 변환할 수 없다고 컴파일러에서 불평하네요 타입 검사 전에 프리프로세서 단계에서 평가하는 C 매크로와는 차이가 있지만 Swift 함수에서 잘 알려져 있고 사랑받는 기능을 전부 사용할 수 있습니다 매크로를 일반화할 수도 있죠
또한 이 매크로는 독립형 표현식 매크로 역할로 선언됩니다 표현식을 사용할 수 있는 곳이라면 매크로를 사용할 수 있으며 #stringify에서 볼 수 있듯이 해시 문자로 표시되죠 다른 종류의 매크로는 선언을 증강할 수 있는 연결 매크로입니다 이건 나중에 다룰게요 컴파일러는 모든 인수가 매크로의 매개변수와 일치하는지 확인한 후 매크로 확장을 수행합니다 단일 매크로 표현식에 집중해서 작동 방식을 살펴봅시다
각 매크로는 확장을 수행하고자 컴파일러 플러그인에서 구현을 정의합니다 컴파일러는 전체 매크로 표현식의 소스 코드를 해당 플러그인으로 보내죠 매크로 플러그인은 제일 먼저 매크로의 소스 코드를 SwiftSyntax 트리로 파싱합니다 이 트리는 소스 코드를 정확하게 반영하는 매크로의 구조적 표현이자 매크로가 작동하는 기반이 되죠
이 트리에서 stringify 매크로는 매크로 확장 표현식 노드로 표현돼요 매크로 이름 stringify가 표현식에 표시됩니다 여기서 취하는 단일 인수는 2 더하기 3에 적용된 중위 연산자 plus예요 Swift 매크로는 뛰어난 강점이 있는데요 매크로 구현 자체가 Swift로 작성된 프로그램이며 원하는 구문 트리로 모든 변환을 수행할 수 있습니다
이번 예시에서는 앞서 봤듯 투플을 생성하죠 이후 생성된 구문 트리를 소스 코드로 다시 직렬화해 컴파일러로 보내면 매크로 표현식이 확장된 코드로 대체됩니다
정말 멋지네요 코드에서는 어떻게 보이는지 확인해 봐야겠어요 Xcode의 새로운 매크로 템플릿은 방금 본 stringify 매크로를 정의합니다 해당 템플릿을 살펴보고 매크로의 정의와 확장 작동 방식 테스트 방법까지 살펴보겠습니다 템플릿을 만들어 봅시다 파일에서 새로 만들기, 패키지를 클릭하고 'Swift 매크로 템플릿'을 선택합니다
첫 번째 매크로 이름은 'WWDC'로 할게요
템플릿으로 뭘 얻을 수 있을까요? #stringify 매크로를 여기서 호출합니다 전에 본 것과 유사하죠 매개변수 a + b를 취하고 결과와 함께 이를 생성한 코드를 반환합니다 매크로가 확장되는 대상을 확인하려면 우클릭해서 '매크로 확장하기'를 선택하세요
앞서 봤던 것과 동일하네요 매크로는 어떻게 정의하는 걸까요? 정의로 넘어가 봅시다
이전 stringify 매크로를 약간 일반화한 버전입니다 이 매크로는 제네릭이라 정수를 취하는 대신 모든 타입 T를 받을 수 있죠
매크로는 외부 매크로로 선언됩니다
확장을 수행하려면 WWDCMacros 모듈에서 StringifyMacro 타입을 보라고 컴파일러에 알리는 거예요
타입은 어떻게 정의할까요?
좀 더 자세히 살펴봅시다 stringify는 독립형 표현식 매크로로 선언되기 때문에 StringifyMacro 타입은 ExpressionMacro 프로토콜을 준수해야 합니다
이 프로토콜의 단일 요구 사항은 expansion 함수입니다 매크로 표현식의 구문 트리와 콘텍스트를 취하는데 이는 컴파일러와 통신할 때 사용 가능하죠
이후 expansion 함수는 재작성된 표현식 구문을 반환합니다
구현에서는 어떤 역할을 할까요? 우선 단일 인수를 매크로 표현식으로 회수합니다 단일 매개변수를 취하도록 stringify를 선언했으므로 해당 인수가 존재하며 매크로 확장을 적용하기 전에 모든 인수를 타입 검사해야 한다는 걸 인지하고 있습니다 이후 문자열 보간을 사용해 투플의 구문 트리를 생성합니다 첫 번째 요소는 인수 자체이고 두 번째 요소는 인수의 소스 코드를 포함하는 문자열 리터럴입니다
여기서 함수는 문자열을 반환하지 않아요 표현식 구문을 반환하죠 매크로는 자동으로 Swift 파서를 호출해서 이 리터럴을 구문 트리로 변환합니다 두 번째 인수에 리터럴 보간 스타일을 사용하므로 리터럴의 콘텐츠를 적절히 이스케이핑할 겁니다 버그는 항상 성가시지만 매크로를 확장해서 명시적으로 요청해야만 나타나는 코드 내 버그는 더더욱 골칫덩이죠 매크로가 제대로 테스트되었는지 꼭 확인해야 합니다 매크로는 부수 효과가 없고 구문 트리의 소스 코드는 비교하기 쉽기 때문에 단위 테스트를 작성해서 매크로를 테스트하면 좋습니다 매크로 템플릿에서 제공하는 단위 테스트가 있어요
이 테스트 케이스는 SwiftSyntax 패키지의 assertMacroExpansion 함수로 stringify 매크로가 올바르게 확장되는지 검증합니다
앞서 봤던 #stringify(a + b) 표현식을 입력으로 취하고 매크로가 확장된 후 a + b와 문자열 리터럴 "a + b"를 포함하는 투플을 생성한다고 단언합니다
테스트 케이스에 매크로 확장 방법을 알리고자 testMacros 매개변수를 전달하는데 이 매개변수는 StringifyMacro 타입을 사용해 매크로 #stringify를 확장해야 한다고 명시하죠 이전과 동일한 방식으로 앱 테스트를 실행해서 실제로 통과하는지 확인해 봅시다
테스트는 통과했습니다 이렇게 첫 매크로를 갖게 됐어요
기본적인 구성 요소를 내부에서 살펴봤습니다 매크로 선언은 매크로의 서명을 정의하고 매크로 역할도 선언합니다 컴파일러 플러그인이 확장을 수행해요 Swift로 작성하고 SwiftSyntax 트리에서 작동하는 프로그램이죠
또한 매크로가 결정적인 구문 트리 변환이고 구문 트리의 소스 코드는 비교하기 쉽기 때문에 테스트 가능하다는 걸 확인했습니다 그렇다면 또 어떤 상황에서 매크로를 사용할 수 있을까요? 독립형 표현식 매크로를 앞서 살펴봤는데요 간단히 말하자면 이 매크로는 해시와 함께 적고 전체 매크로 표현식을 다시 작성할 수 있습니다 표현식 대신 선언으로 확장하는 독립 선언 역할도 있어요 다른 종류의 매크로는 연결 매크로입니다 속성과 마찬가지로 @와 함께 표기하며 연결된 선언을 매크로가 증강하도록 합니다 예를 들어 연결 멤버 매크로는 연결된 타입의 새 멤버를 추가하죠 다른 역할을 더 알고 싶다면 'Swift 매크로 상세히 알아보기'를 참고하세요 베카가 해당 세션에서 관련 내용을 자세히 소개합니다 저는 오늘 연결 멤버 역할을 중점적으로 다룰 겁니다 현재 작업 중인 앱의 코드베이스를 개선하는 데 도움이 됐거든요 저는 스키 강사로도 일해요 학생들과 함께 갈 여행을 계획하는 데 쓸 앱을 최근에 개발 중입니다
스키 강사라면 너무 어려운 슬로프에 초보자를 데려가지 않도록 유의해야겠죠 Swift 타입 시스템을 사용해서 이를 적용해 보려고 합니다 그래서 제가 좋아하는 스키장의 슬로프를 모두 포함하는 Slope 열거형 외에도 초보자용 슬로프만 포함하는 EasySlope 타입도 있습니다 이니셜라이저는 슬로프 난이도가 쉬우면 쉬운 슬로프로 변환하고 연산 프로퍼티는 쉬운 슬로프를 다시 일반 슬로프로 변환하죠
훌륭한 타입 안전성을 제공하지만 너무 반복적입니다 쉬운 슬로프를 추가하려면 Slope와
EasySlope 이니셜라이저 연산 프로퍼티에 모두 추가해야 하죠 매크로를 사용해 이를 개선할 수 있을지 알아봅시다 이니셜라이저와 연산 프로퍼티를 자동으로 생성하고자 합니다 어떻게 하면 될까요? 이니셜라이저와 연산 프로퍼티는 모두 EasySlope 타입 멤버이므로 연결 멤버 매크로를 선언해야 합니다
이후 매크로의 구현을 포함하는 컴파일러 플러그인을 생성해요
매크로가 예상대로 작동하도록 테스트 기반 방식으로 개발하고자 합니다 따라서 테스트 케이스를 작성할 때까지 구현을 비워 둡니다
테스트 케이스에서 매크로의 동작을 정의한 후 해당 테스트 케이스와 일치하도록 구현을 작성할 겁니다
마지막으로 새 매크로를 앱에 통합합니다 전부 순조롭게 진행되면 이니셜라이저를 제거하고 매크로에서 생성하도록 설정할 수 있겠죠
이전에 만든 템플릿으로 작업해서 매크로를 개발해 봅시다 앱에 별로 필요하지 않은 #stringify 매크로는 이미 제거했습니다 @attached(member) 속성을 사용해 새 연결 멤버 매크로를 선언할게요
SlopeSubset으로 명명했어요 EasySlope는 Slope의 부분집합이니까요
또한 매크로는 소개하는 멤버 이름도 정의합니다
이번 시연에서는 이니셜라이저 생성 방법을 보여 드리겠습니다 연산 프로퍼티 생성과 매우 유사하죠 마찬가지로 모든 케이스를 전환하는 switch문이니까요 이 선언을 통해 매크로를 정의했지만 실제로 수행하는 확장은 아직 구현하지 않았습니다
이를 위해 매크로는 WWDCMacros 모듈의 SlopeSubsetMacro 타입을 참조합니다 어서 해당 타입을 생성해서 정말 재밌는 부분으로 넘어가 보도록 하죠 매크로 구현 말이에요
SlopeSubset을 연결 멤버 매크로로 선언했으므로 해당 구현은 MemberMacro 프로토콜을 준수해야 합니다
이 프로토콜에는 단일 요구 사항이 있는데요 바로 expansion 함수입니다 ExpressionMacro와 유사하죠
expansion 함수는 매크로를 선언에 적용하는 속성과 매크로가 적용되는 선언을 취해요 이 케이스에서는 EasySlope 열거형 선언이 되겠죠
그런 다음 매크로는 해당 선언에 추가할 모든 새 멤버 목록을 반환합니다
이제 곧바로 변환을 구현하고 싶더라도 테스트 케이스 먼저 작성하기로 했죠
그러니 일단 빈 배열을 반환하고 새 멤버를 추가할 필요가 없다고 표시해 줄게요
마지막으로 컴파일러에서 SlopeSubset이 보이도록 해 보죠 여기 아래 보이는 providingMacros 프로퍼티에 추가하겠습니다
계속 진행하기 전에 지금까지 작업한 내용이 잘 작동하는지 확인해 봅시다 Xcode에서 매크로를 적용하고 확장된 코드를 살펴볼 수도 있지만 회귀 분석을 도입하지 않도록 매크로를 변경할 때마다 다시 실행할 수 있는 테스트 케이스를 작성하는 게 훨씬 더 좋습니다
템플릿의 테스트 케이스처럼 assertMacroExpansion 함수로 매크로의 동작을 검증합니다
EasySlope 타입에 적용될 때 매크로가 무엇을 생성하는지 테스트하고자 하므로 테스트 케이스의 입력으로 이를 사용합니다
매크로가 아직 아무 작업도 수행하지 않기 때문에 속성을 제거하고 새 멤버를 추가하지 않을 거예요 따라서 예상되는 확장 코드는 입력과 동일합니다 @SlopeSubset만 빼면 되죠
마지막으로 테스트 케이스에서 SlopeSubsetMacro 구현을 사용해 매크로 SlopeSubset을 확장해야 합니다 매크로 이름을 testMacros 딕셔너리에서 구현 타입에 매핑하고 해당 딕셔너리를 assertion 함수에 전달하면 돼요
이제 테스트를 실행해서 지금까지 작성한 내용이 잘 작동하는지 확인해 봅시다
잘 작동하는군요 아주 좋아요 하지만 실질적인 목표는 단순히 속성을 제거하는 게 아니라 매크로가 실제로 이니셜라이저를 생성하는지 확인하는 겁니다 이전에 손으로 작성한 코드를 테스트 케이스로 복사하겠습니다 플러그인이 생성하길 바라는 게 바로 해당 코드니까요
테스트를 다시 실행해 보죠
작동하지 않는군요 매크로가 이니셜라이저를 아직 생성하지 않으니까요
이제 변경해 보겠습니다
이니셜라이저는 EasySlopes 열거형에 선언된 모든 열거형 요소를 전환합니다 따라서 가장 먼저 할 일은 선언에서 이런 열거형 요소를 회수하는 겁니다 열거형 선언 내에서만 열거형 요소를 선언할 수 있으므로 우선 열거형 선언에 선언을 캐스팅해야겠죠
열거형이 아닌 타입에 매크로가 연결됐다면 오류를 내보내야 합니다 나중에 잊어버리지 않도록 TODO를 추가하고 일단 빈 배열을 반환합니다 이어서 열거형이 선언하는 요소를 모두 가져와야 해요 SwiftSyntax 트리에서 열거형의 구문 구조를 검사해 방법을 찾아봐야겠습니다
매크로 구현은 일반적인 Swift 프로그램이므로 Xcode에서 알려진 도구를 총동원해 프로그램을 디버깅할 수 있어요 예를 들어 expansion 함수 내부에 중단점을 설정하고 해당 중단점에 도달하도록 테스트 케이스를 실행할 수 있죠
이제 매크로 구현 내부에서 디버거가 일시 중지 됐으며 enumDecl은 EasySlopes 열거형입니다 po enumDecl을 입력해 디버거에서 출력할 수 있어요
출력을 검사해 봅시다
구문 트리의 가장 안쪽 노드는 열거형 요소인 beginnersParadise와 practiceRun 슬로프를 표현합니다 두 요소를 회수하려면 구문 트리에 요약된 구조를 따라야 합니다 이 구조를 단계별로 살펴보고 액세스 코드를 작성해 보겠습니다
열거형 선언에는 자식 memberBlock이 있습니다 이 멤버 블록은 중괄호와 실제 멤버를 포함합니다 멤버에 액세스하려면 enumDecl.memberBlock.members로 시작해야겠죠
이 멤버는 실제 선언과 선택적 세미콜론을 포함해요 우리의 관심사는 선언입니다 특히 열거형 케이스를 실제로 선언하는 선언에 관심이 많죠 열거형 케이스인 모든 멤버 선언의 목록을 얻고자 콤팩트 맵을 사용하고 있습니다 케이스 선언마다 여러 요소를 선언할 수 있어요 슬로프마다 줄을 바꿔 개별 case 키워드 뒤에 선언하는 대신 같은 줄에 작성할 수 있었기 때문이죠 case beginnersParadise에 연이어 practiceRun을 적는 식으로요
모두 회수하려면 flatMap을 사용하면 됩니다
이제 모든 요소를 회수했으므로 EasySlope에 추가할 이니셜라이저를 구축할 수 있습니다
이니셜라이저 선언에는 단일 항목이 있습니다 바로 switch 표현식인데요
switch 표현식은 열거형의 각 요소에 대한 케이스와 nil을 반환하는 기본 케이스를 포함합니다 모든 케이스에 대한 구문 노드를 생성해야 해요
생성할 구문 노드를 찾을 때 유용한 두 가지 방법이 있는데요 전에 해 봤듯이 구문 트리를 출력하거나 SwiftSyntax 문서를 읽는 겁니다
InitializerDeclSyntax부터 구축해 보도록 하죠
이 타입은 결과 빌더를 사용해 본문을 빌드하고 헤더를 지정해 구성할 수 있습니다 헤더는 init 키워드와 모든 매개변수가 되겠죠 이렇게 하면 결과 빌더에서 for 루프를 사용해 정확히 필요한 요소를 전부 반복할 수 있습니다
테스트 케이스에서 init 헤더를 복사할게요
본문 내부에는 switch 표현식이 필요합니다
이 타입에는 헤더와 결과 빌더를 취하는 이니셜라이저도 있어요 다시 사용합시다
이제 앞서 수집한 모든 요소를 반복해서 결과 빌더의 기능을 활용할 수 있어요
#stringify에서 봤듯이 각 요소에 대해 문자열 보간을 사용해 구성할 수 있는 새로운 케이스 항목을 생성하려고 합니다
nil을 반환하는 기본 케이스도 추가해야겠죠
마지막으로 이니셜라이저를 반환할 수 있습니다
테스트를 실행해서 올바른 이니셜라이저를 생성하는지 확인해 봅시다
잘되는군요 매크로가 잘 작동합니다 앱에서 사용해도 되겠어요
Xcode 프로젝트에 매크로 패키지를 추가하려면 우클릭한 뒤 '패키지 종속성 추가하기'를 선택합니다 방금 생성한 로컬 페이지를 이제 고를 수 있습니다
매크로를 사용할 수 있도록 WWDC 타깃을 앱의 종속성으로 추가할게요
이제 패키지에서 WWDC 모듈을 가져오고 SlopeSubset 매크로를 EasySlope 타입에 적용 가능해요
...
빌드해 봤더니 컴파일러가 불평하는군요 직접 작성한 이니셜라이저가 무효화된 재선언이라고요 이제 매크로가 생성하기 때문이죠 그러니까 삭제해도 됩니다
코드를 삭제하면 늘 짜릿하지 않나요? 매크로가 실제로 생성한 내용을 보려면 SlopeSubset을 우클릭해 '매크로 확장하기'를 클릭합니다
매크로 기능을 잊어버렸다면 option 키를 누른 채 클릭해 문서를 읽을 수 있습니다
연산 프로퍼티를 이어서 생성해야겠지만 그건 잠시 뒤에 할게요 매크로를 사용해서 반복적인 코드를 작성하지 않고 EasySlopes의 타입 안전성을 얻을 수 있었어요 어떻게 했었죠? Swift 매크로 패키지 템플릿으로 시작했습니다 구문 트리 구조를 둘러보려고 매크로 실행을 중단하고 디버거 내부에 구문 노드를 출력했죠 이렇게 해서 모든 열거형 요소를 얻을 때 액세스해야 하는 프로퍼티를 확인할 수 있었어요
테스트 케이스를 사용해 매크로를 자체적으로 개발하는 건 정말 쉬웠습니다 앱에 추가했더니 바로 작동했죠 매크로가 지원하지 않는 기능에 매크로를 사용하면 어떻게 될까요? 어려운 슬로프로 초보자를 데려가는 게 바람직하지 않듯 매크로로 예상치 못한 확장을 수행하거나 컴파일하지 않는 코드를 만드는 건 바람직하지 않죠 매크로가 지원하지 않는 방식으로 사용되는 경우 채택자가 생성된 코드를 읽어 매크로를 디버그하도록 처리하는 대신 항상 오류 메시지를 내보내서 뭐가 잘못되었는지 알려 줍니다
그런 의미에서 코드베이스에 남겨 뒀던 TODO를 수정해 보죠 열거형이 아닌 타입에 SlopeSubset이 적용되면 매크로는 오류를 내보내서 열거형에만 적용할 수 있다고 안내합니다 이전과 마찬가지로 테스트 케이스부터 추가합시다
이번에는 구조체에 SlopeSubset 매크로를 적용할게요
구조체에 열거형 요소가 없기 때문에 매크로에서 이니셜라이저를 생성하지 않겠죠 대신 진단을 내보내야 합니다 SlopeSubset은 열거형에만 적용될 수 있다고 오류 메시지를 보내 알리는 거죠 테스트를 실행해 볼게요 실패로군요 오류 메시지가 아직 출력되지 않아요 컴파일러 플러그인으로 가서 문제를 해결해 봅시다
매크로 오류는 Swift Error 프로토콜을 준수하는 모든 타입으로 표현할 수 있습니다 열거형이 아닌 타입에 SlopeSubset을 적용하는 경우 단일 케이스가 있는 열거형으로 오류 메시지를 설명합니다
expansion 함수에서 오류를 발생시키면 매크로 확장을 호출하는 속성에 표시됩니다
속성이 아닌 다른 위치에 오류 메시지를 표시하거나 경고를 생성하거나 Xcode의 Fix-It을 표시하려면 콘텍스트 매개변수에 addDiagnostic 메서드가 있습니다 풍부한 진단을 생성하는 메서드죠 하지만 이번 케이스에서는 간단한 오류 메시지를 속성에 표시하는 게 효율적이라고 생각합니다 전부 알맞게 작성했는지 또 테스트는 통과하는지 확인해 보죠
좋네요, 통과했습니다 SlopeSubset을 구조체에 적용하면 Xcode에서 어떻게 보일까요? 테스트 케이스를 파일로 복사해 볼게요
Xcode는 여타 모든 컴파일 오류와 함께 커스텀 오류 메시지를 표시합니다 매크로 채택자가 잘못한 부분을 확인하기 쉽죠
이제 이 매크로는 오류를 잘 처리하므로 슬로프뿐만 아니라 열거형 부분집합을 지정하는 타 개발자에게도 유용할 거라고 생각해요 이제 일반화해 보겠습니다
여태 Slope로 하드 코딩 했던 열거형 상위집합을 지정하려면 제네릭 매개변수를 매크로 선언에 추가합니다
매크로가 더는 슬로프에만 적용되지 않으므로 EnumSubset으로 이름을 바꿔 줄게요 SlopeSubset을 우클릭한 뒤 '리팩터링하기'로 들어가 '이름 변경하기'를 선택합니다
command 키를 누른 채로 클릭해서 문자열 리터럴과 주석에서 생기는 모든 항목의 이름을 바꿀 수도 있어요
이제 하드 코딩 된 Slope 타입 대신 제네릭 매개변수를 사용하도록 매크로 구현을 조정해야 합니다 enumDecl에서처럼 디버거 내부에서 속성을 출력하고 레이아웃을 검사하면 제네릭 매개변수를 회수할 수 있죠 속성 이름의 genericArgumentClause에서 첫 번째 인수의 argumentType에 액세스해서요 제네릭 매개변수를 회수했으므로 지금까지 하드 코딩 했던 Slope 타입을 변수 supersetType으로 대체할 수 있어요
추가로 변경할 사항이 몇 가지 더 있습니다 이니셜라이저의 매개변수와 매크로 구현 타입을 다시 명명하고 문서도 업데이트해야 하는데 나중에 할게요 일단 지금은 테스트가 여전히 통과되는지 보죠
EnumSubset을 제네릭으로 만들었으므로 EnumSubset 매크로에 제네릭 매개변수로 슬로프를 전달하여 EasySlope가 슬로프의 하위 집합임을 명시적으로 지정해야 합니다
테스트가 전부 통과하는지 확인해 봅시다
통과하는군요 이 매크로를 Swift 패키지로 발행할지 진지하게 고민해 봐야겠어요 오늘은 많은 내용을 다뤘습니다 배운 걸 요약해 봅시다 매크로 패키지 템플릿으로 매크로를 생성하면 stringify 매크로를 포함하므로 손쉽게 시작할 수 있습니다 매크로를 개발하는 동안 테스트 케이스를 꼭 작성해서 매크로가 생성하는 코드가 유효한지 확인해 보세요 이렇게 진행하면서 expansion 함수에 중단점을 설정하고 테스트를 실행한 다음 디버거에서 구문 트리를 출력해 구문 트리의 레이아웃을 검사할 수 있습니다 마지막으로 특정 상황에서 매크로를 적용할 수 없다면 커스텀 오류 메시지를 항상 내보내야 합니다 문제가 발생하더라도 매크로가 빛을 발하도록요 시청해 주셔서 감사합니다 매크로를 마음껏 생성해 보세요 ♪ ♪
-
-
5:55 - Invocation of the stringify macro
import WWDC let a = 17 let b = 25 let (result, code) = #stringify(a + b) print("The value \(result) was produced by the code \"\(code)\"")
-
6:31 - Declaration of the stringify macro
@freestanding(expression) public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "WWDCMacros", type: "StringifyMacro")
-
7:10 - Implementation of the stringify macro
public struct StringifyMacro: ExpressionMacro { public static func expansion( of node: some FreestandingMacroExpansionSyntax, in context: some MacroExpansionContext ) -> ExprSyntax { guard let argument = node.argumentList.first?.expression else { fatalError("compiler bug: the macro does not have any arguments") } return "(\(argument), \(literal: argument.description))" } }
-
9:12 - Tests for the stringify Macro
final class WWDCTests: XCTestCase { func testMacro() { assertMacroExpansion( """ #stringify(a + b) """, expandedSource: """ (a + b, "a + b") """, macros: testMacros ) } } let testMacros: [String: Macro.Type] = [ "stringify": StringifyMacro.self ]
-
12:05 - Slope and EasySlope
/// Slopes in my favorite ski resort. enum Slope { case beginnersParadise case practiceRun case livingRoom case olympicRun case blackBeauty } /// Slopes suitable for beginners. Subset of `Slopes`. enum EasySlope { case beginnersParadise case practiceRun init?(_ slope: Slope) { switch slope { case .beginnersParadise: self = .beginnersParadise case .practiceRun: self = .practiceRun default: return nil } } var slope: Slope { switch self { case .beginnersParadise: return .beginnersParadise case .practiceRun: return .practiceRun } } }
-
14:16 - Declare SlopeSubset
/// Defines a subset of the `Slope` enum /// /// Generates two members: /// - An initializer that converts a `Slope` to this type if the slope is /// declared in this subset, otherwise returns `nil` /// - A computed property `slope` to convert this type to a `Slope` /// /// - Important: All enum cases declared in this macro must also exist in the /// `Slope` enum. @attached(member, names: named(init)) public macro SlopeSubset() = #externalMacro(module: "WWDCMacros", type: "SlopeSubsetMacro")
-
15:24 - Write empty implementation for SlopeSubset
/// Implementation of the `SlopeSubset` macro. public struct SlopeSubsetMacro: MemberMacro { public static func expansion( of attribute: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { return [] } }
-
16:23 - Register SlopeSubsetMacro in the compiler plugin
@main struct WWDCPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ SlopeSubsetMacro.self ] }
-
18:41 - Test SlopeSubset
let testMacros: [String: Macro.Type] = [ "SlopeSubset" : SlopeSubsetMacro.self, ] final class WWDCTests: XCTestCase { func testSlopeSubset() { assertMacroExpansion( """ @SlopeSubset enum EasySlope { case beginnersParadise case practiceRun } """, expandedSource: """ enum EasySlope { case beginnersParadise case practiceRun init?(_ slope: Slope) { switch slope { case .beginnersParadise: self = .beginnersParadise case .practiceRun: self = .practiceRun default: return nil } } } """, macros: testMacros ) } }
-
19:25 - Cast declaration to an enum declaration
guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { // TODO: Emit an error here return [] }
-
21:14 - Extract enum members
let members = enumDecl.memberBlock.members
-
21:32 - Load enum cases
let caseDecls = members.compactMap { $0.decl.as(EnumCaseDeclSyntax.self) }
-
21:58 - Retrieve enum elements
let elements = caseDecls.flatMap { $0.elements }
-
24:11 - Generate initializer
let initializer = try InitializerDeclSyntax("init?(_ slope: Slope)") { try SwitchExprSyntax("switch slope") { for element in elements { SwitchCaseSyntax( """ case .\(element.identifier): self = .\(element.identifier) """ ) } SwitchCaseSyntax("default: return nil") } }
-
24:19 - Return generated initializer
return [DeclSyntax(initializer)]
-
25:51 - Apply SlopeSubset to EasySlope
/// Slopes suitable for beginners. Subset of `Slopes`. @SlopeSubset enum EasySlope { case beginnersParadise case practiceRun var slope: Slope { switch self { case .beginnersParadise: return .beginnersParadise case .practiceRun: return .practiceRun } } }
-
28:00 - Test that we generate an error when applying SlopeSubset to a struct
func testSlopeSubsetOnStruct() throws { assertMacroExpansion( """ @SlopeSubset struct Skier { } """, expandedSource: """ struct Skier { } """, diagnostics: [ DiagnosticSpec(message: "@SlopeSubset can only be applied to an enum", line: 1, column: 1) ], macros: testMacros ) }
-
28:48 - Define error to emit when SlopeSubset is applied to a non-enum type
enum SlopeSubsetError: CustomStringConvertible, Error { case onlyApplicableToEnum var description: String { switch self { case .onlyApplicableToEnum: return "@SlopeSubset can only be applied to an enum" } } }
-
29:09 - Throw error if SlopeSubset is applied to a non-enum type
throw SlopeSubsetError.onlyApplicableToEnum
-
31:03 - Generalize SlopeSubset declaration to EnumSubset
@attached(member, names: named(init)) public macro EnumSubset<Superset>() = #externalMacro(module: "WWDCMacros", type: "SlopeSubsetMacro")
-
31:33 - Retrieve the generic parameter of EnumSubset
guard let supersetType = attribute .attributeName.as(SimpleTypeIdentifierSyntax.self)? .genericArgumentClause? .arguments.first? .argumentType else { // TODO: Handle error return [] }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.