스트리밍은 대부분의 브라우저와
Developer 앱에서 사용할 수 있습니다.
-
Swift 매크로 상세히 알아보기
Swift 매크로가 코드베이스에서 어떻게 보일러플레이트를 줄이고 복잡한 기능을 더 쉽게 도입하게 하는지 알아보세요. 매크로가 어떻게 코드를 분석하는지, 풍부한 컴파일러 오류 메시지를 발신해 개발자들이 올바르게 사용하도록 돕는지, 그리고 프로젝트로 저절로 다시 통합되는 새 코드를 생성해 주는지 살펴봅니다. 매크로 역할과 컴파일러 플러그인, 구문 트리와 같은 중요한 개념도 함께 알아보세요.
챕터
- 0:00 - Introduction
- 0:51 - Why macros?
- 2:13 - Design philosophy
- 4:48 - Translation model
- 6:18 - Macro roles
- 17:48 - Macro implementation
- 33:36 - Writing correct macros
- 38:42 - Wrap up
리소스
관련 비디오
WWDC23
-
다운로드
♪ ♪
안녕하세요 Swift 팀 소속 베카입니다 오늘 함께 볼 흥미로운 새 기능은 Swift 매크로로서 여러분의 필요에 맞춰 Swift 언어를 사용자화하게 합니다 매크로의 쓰임에 관해 먼저 얘기해 볼게요 그다음에는 Swift 매크로를 디자인했을 때 염두에 두었던 원칙을 말씀드리겠습니다 그 후엔 Swift 매크로의 작동 방식 및 프로젝트의 다른 코드와 상호 작용하는 특정 방식을 보죠
이후에는 매크로의 구현법에 관해 얘기하고 마지막으로는 매크로가 제대로 작동하게 하는 법을 논하겠습니다
Swift가 매크로를 지원하는 이유를 먼저 다뤄보죠 Swift는 사용자가 표현이 풍부한 코드와 API를 쓰는 걸 선호합니다 그렇기 때문에 사용자가 반복되는 보일러플레이트 작성을 피하게 도와주는 파생된 준수나 결과 빌더 같은 기능을 지원하는 것이죠 이 기능은 거의 같은 식으로 작동합니다 예를 들어 멤버를 위한 구현을 제공하지 않고 Codable을 준수하면 Swift는 자동으로 이를 일련의 멤버에 전개하여 프로그램에 삽입되게 할 것입니다 회색 박스 안에서 준수에 대한 전개식을 보여드렸습니다 여러분을 위해 이 코드를 만들어서 Codable의 작동법을 잘 몰라도 쓸 수 있게 했으며 Codable 지원을 추가하는 것과 화면 가득히 코드를 쓰는 걸 두고 고민할 필요가 없게 했죠 Swift는 이런 식으로 작동하는 기능이 많이 있답니다 간단한 구문을 작성하면 컴파일러에서 자동으로 이를 더 복잡한 코드로 전개하죠 하지만 기존의 기능으로 원하는 걸 구현할 수 없다면요? Swift 컴파일러에 기능을 더할 수 있겠죠 오픈 소스니까요 하지만 그러려면 말 그대로 제가 개인적으로 화상 회의에 들어가 다른 Swift 프로젝트 리더와 함께 여러분이 만든 기능을 토론해야만 하기에 확장성이 떨어진다고 할 수 있죠 그래서 저희가 매크로를 소개하는 겁니다 매크로는 컴파일러를 수정할 필요 없이 패키지 속에 배포하는 방식으로 Swift에 여러분만의 언어 기능을 추가하게 하여 지루함과 보일러플레이트를 제거합니다 다른 언어에서 매크로를 써 보지 않은 분도 있을 텐데요 만일 써 봤다면 이것이 좋은지 확신할 수 없을 수 있죠 많은 Swift 개발자가 Objective-C나 C 프리프로세서를 쓰는 다른 언어에 친숙하면서 C 매크로의 한계와 함정을 알고 있으니까요 하지만 Swift 매크로는 매우 달라서 이런 문제를 상당수 피할 수 있습니다 네 가지 목표를 염두에 두고 매크로를 디자인했는데요 첫 번째 목표는 매크로를 쓸 때 명확해야 한다는 것입니다 매크로는 두 종류가 있죠 독립형 매크로는 코드에서 무언가 다른 것 대신 위치합니다 이 매크로는 항상 # 기호로 시작하죠 첨부 매크로는 코드에 있는 선언의 속성으로 쓰입니다 이것은 모두 @ 기호로 시작합니다 Swift는 이미 #와 @ 기호를 이용해 특별한 컴파일러 동작을 나타내며 매크로가 이를 확장성 있게 만드는 겁니다 #나 @가 보이지 않는다면 매크로가 전혀 쓰이지 않았다고 생각하셔도 됩니다 둘째 목표는 매크로에 넘긴 코드와 매크로에서 넘어온 코드 둘 다 완전해야 하며 잘못을 확인해야 한다는 겁니다 '1 +'는 매크로에 넘길 수 없는데 인수가 완전한 표현식이어야 하기 때문이죠 또한 잘못된 유형을 가진 인수를 넘길 수도 없는데 매크로 인수 및 결과가 함수 인수와 똑같이 유형 검사를 받기 때문입니다 매크로의 구현은 그 입력값을 검증하고 잘못된 경우 컴파일러 경고나 오류 메시지를 내서 매크로를 올바르게 쓰고 있는지 확인하기 더 쉽습니다 셋째 목표는 매크로 전개가 예측 가능하고 부가적인 방식으로 프로그램에 포함돼야 한다는 겁니다 매크로는 프로그램에서 보이는 코드에만 추가할 수 있습니다 매크로는 코드를 없애거나 바꿀 수 없죠 따라서 'someUnknownMacro'의 용도가 뭔지 모르더라도 이것이 'finishDoingThingy'로의 호출을 지우거나 새로운 함수로 옮길 수 없는 건 확신할 수 있답니다 덕분에 매크로를 사용하는 코드를 읽는 게 훨씬 더 쉬워지죠 마지막 목표는 매크로가 무적의 마법이 돼서는 안 된다는 겁니다 매크로는 프로그램에 코드를 더할 뿐이고 Xcode에서 이를 바로 볼 수 있죠 여러분은 매크로의 사용 위치를 우클릭해 어디로 전개되는지 볼 수 있습니다 전개식 내에 중단점을 설정하거나 디버거로 끼어들 수 있죠 매크로 전개 내부의 코드가 컴파일되지 않을 때는 전개식에서 오류의 위치뿐만 아니라 소스 코드에서 어디로 전개되는지도 볼 수 있습니다 매크로가 폐쇄형 소스 라이브러리에서 제공되는데도 이 모든 도구가 작동합니다 매크로 작성자는 매크로에 대한 단위 테스트를 작성하여 예상대로 작동하는지 확인할 수 있고 저희는 이를 적극 권장합니다 이 목표 덕분에 개발자가 Swift 매크로를 더 잘 이해하고 유지할 수 있다고 봅니다 Swift 매크로의 성취 목표를 보았으니 이제 실제 작동 방식을 보죠 세부 설명에 들어가기 전에 먼저 기본 개념을 짚어볼게요 Xcode 매크로 패키지 템플릿의 stringify 매크로처럼 매크로를 코드에 호출하는 걸 Swift에서 인식하면 Swift는 코드에서 그 용도를 추출하여 그 매크로의 구현을 포함하는 특수한 컴파일러 플러그인에 보냅니다 플러그인은 보안 샌드박스 내에서 별도의 프로세스로서 실행되고 매크로의 작성자가 작성한 커스텀 Swift 코드를 포함하죠 이것은 매크로 용도를 처리하여 전개식을 반환하는데 바로 매크로가 만든 새 코드 조각입니다 그 후 Swift 컴파일러는 이 전개식을 프로그램에 추가하고 여러분의 코드와 전개식을 함께 컴파일합니다 그래서 프로그램을 실행하면 매크로를 호출한 대신에 여러분이 직접 작성한 것처럼 전개식이 잘 작동하죠 여기에 제가 주석을 단 중요한 요점이 있는데요 어떻게 Swift는 'stringify' 매크로의 존재를 알았을까요? 그 답은 바로 매크로 선언에서 알았다는 것입니다 매크로 선언은 매크로를 위한 API를 제공합니다 여러분만의 모듈에 선언을 작성할 수도 있고 이를 라이브러리나 프레임워크에서 가져올 수도 있죠 선언은 매크로의 이름과 서명 및 가지는 매개변수의 수, 레이블 유형을 지정하며 만일 있다면 결과의 유형도 지정합니다 함수 선언과 똑같이요 또한 매크로의 역할을 지정하는 한 개 이상의 속성도 가집니다 그 역할이 무엇인지 생각지 않고 매크로를 작성할 수는 없죠 그러니 역할이 무엇인지와 다양한 매크로를 작성하기 위해 역할을 이용하는 방법을 알아볼게요 역할이란 매크로에 대한 일련의 규칙을 말합니다 이것은 매크로를 어디에, 어떻게 적용할지와 어떤 코드로 전개되는지 이 전개식이 코드의 어디에 삽입될지를 결정합니다 궁극적으로 예측 가능하고 부가적인 방식으로 전개식을 삽입하려는 목표를 달성하는 것이 매크로 역할입니다 독립형 매크로를 만드는 두 가지 역할이 있는데 바로 표현과 선언입니다 첨부 매크로를 만드는 역할은 다섯 가지가 있는데 peer, accessor, memberAttribute member, conformance입니다 이 역할들과 그 사용 시기를 함께 봅시다 먼저 독립형 표현식 역할부터 시작하죠 '표현식'이라는 용어를 모르는 분께 설명해 드리자면 표현식은 결과를 실행하고 산출하는 코드 단위를 말합니다 이 'let'문에서 = 기호 뒤의 연산이 표현식이죠 하지만 표현식은 되풀이되는 구조로 되어 있습니다 종종 더 작은 표현식으로 구성돼 있죠 따라서 'x + width'만으로도 표현식이 됩니다 'width' 단어 하나도 표현식이고요 독립형 표현식 매크로는 표현식 안으로 전개되는 매크로인데요 이걸 어떻게 사용할까요? 옵셔널을 강제로 언래핑해야 한다고 가정해 보세요 Swift는 강제 언래핑 연산자를 제공하지만 어떤 팀은 안전을 생각하지도 않고 강제 언래핑을 쓰는 건 너무 대충 넘기는 거라고 생각합니다 이들의 스타일 가이드는 왜 값이 절대 nil이 되면 안 되는지 보여주는 더 복잡한 것을 개발자가 작성하도록 하죠 하지만 'guard let'을 쓴 후 'preconditionFailure'를 'else' 브랜치에서 호출하는 등의 대안 대부분은 좀 너무 거창합니다 이 양극단 사이에서 더 균형을 잘 잡는 매크로를 디자인해 보죠 이 매크로가 연산 후에 값을 반환하길 바라기 때문에 이것을 'freestanding(expression)' 매크로로 만듭니다 'unwrap'이라는 이름과 전달되는 값은 선택적이지만 반환되는 값은 선택적이지 않은 제네릭 유형을 부여합니다 또한 unwrap이 실패할 경우 출력되는 메시지의 일부인 문자열도 전달합니다 따라서 함수처럼 호출하는 매크로를 결국 갖게 되지만 이것은 클로저로 둘러싸인 guard let을 포함하는 표현식으로 전개되죠 오류 메시지는 심지어 변수명도 포함하는데 보통의 함수로는 불가능했을 일입니다 독립형 표현식 역할을 보았으니 이제 독립형 선언 역할을 보죠 이것은 마치 함수나 변수, 유형처럼 하나 이상의 선언으로 전개되는데요 이를 어디에 쓸 수 있을까요? 2D 어레이 유형이 필요한 일종의 통계적 분석을 작성 중이라고 해봅시다 여러분은 어레이의 모든 행에 같은 수의 열이 있길 원하고 어레이로 이루어진 어레이를 원치 않습니다
대신 엘리먼트를 평면인 1차원 어레이에 저장한 후에 개발자가 넣은 2차원 인덱스들에서 1차원 인덱스를 산출하길 원하죠 그러려면 이런 유형을 작성해야 할 수도 있습니다 'makeIndex' 함수는 2D 인덱스에 필요한 정수 두 개를 가져가서 계산을 약간 하여 이를 1D 인덱스로 바꾸죠 하지만 그때 프로그램의 다른 부분에서는 3차원 어레이가 필요하다는 걸 알게 됩니다 2D 어레이와 거의 똑같은 경우죠 인덱스가 조금 더 있고 계산이 약간 더 복잡할 뿐입니다 그 후에는 4D 어레이가 다음에는 5D 어레이가 필요해지고 곧 거의 똑같은 어레이 유형 속에서 헤엄치게 되겠지만 여전히 제네릭이나 프로토콜 확장 또는 하위 클래스 등 Swift가 이런 일에 제공하는 다른 기능을 이용할 만한 정도가 되진 않을 거예요 다행히도 이 구조체 각각은 선언이기 때문에 이들을 생성하기 위해 선언 매크로를 이용할 수 있습니다 그러니 'makeArrayND'라는 이름의 독립형 선언 매크로를 선언해 봅시다 N차원 어레이 유형을 만들 거라서 이름을 이렇게 정했습니다 차원의 개수를 Int 매개변수로서 전달하고 결과 유형은 선언하지 않을 건데요 이 매크로는 선언을 프로그램에 추가하는 것이지 다른 코드가 사용하는 결과를 산출하지 않기 때문입니다 저희는 2, 3, 4, 5차원을 가지고 매크로를 네 번 호출할 수 있고 각 호출은 적절한 수의 인수와 크기에 맞는 연산을 가진 다중 차원의 어레이 유형으로 전개될 겁니다 지금까지는 독립형 매크로만 봤는데요 이제 첨부 매크로를 위한 역할로 넘어갑시다 첨부 매크로는 그 이름처럼 특정 선언에 첨부돼 있습니다 즉 작업해야 할 정보가 더 많이 포함돼 있죠 독립형 매크로는 오로지 전달된 인수만을 받지만 첨부 매크로는 연결된 선언에 접근할 수도 있습니다 이 매크로는 종종 선언을 조사하고 이름과 유형 및 다른 정보를 속에서 끌어내죠 먼저 첨부 peer 역할부터 시작하죠 peer 매크로는 어떤 선언에든 첨부될 수 있는데 이는 변수와 함수 유형뿐만 아니라 가져오기나 연산자 선언까지 아우르며 그 옆에 새 선언을 삽입할 수도 있습니다 따라서 이를 메서드나 프로퍼티에 사용하면 그 유형의 멤버를 생성하게 될 겁니다 하지만 이를 최상급 함수나 유형에 사용하면 새로운 최상급 선언을 만들게 되죠 덕분에 융통성을 크게 발휘할 수 있는데요 이를 쓸 방법 하나를 알려드릴게요 Swift 동시성을 이용하는 라이브러리를 작성한다고 해보죠 하지만 클라이언트 일부는 여전히 옛 동시성 기술을 사용하고 있고 그래서 완료 핸들러를 이용하는 API 버전을 주려고 합니다 이 메서드를 작성하는 건 어렵지 않죠 그저 'async' 키워드를 제거하고 완료 핸들러 매개변수를 더한 후 결과 유형을 매개변수 목록 안에 넣고 분리된 작업에 비동기 버전을 호출하면 됩니다 하지만 많이 반복하다 보니 수작업으로 작성하고 싶지 않습니다 이럴 때 첨부 peer 매크로가 훌륭하게 처리할 수 있죠 'AddCompletionHandler'를 선언하고 여기에 완료 핸들러의 인수 레이블에 대한 매개변수를 줍니다 그 후 이 매크로를 메서드의 비동기 버전에 첨부하세요 매크로는 완료 핸들러에 기반한 원본과 동등한 서명을 생성하고 메서드 본문을 작성한 다음 심지어 완료 핸들러에 대한 추가 텍스트가 있는 문서 주석까지 달 겁니다 꽤 멋지죠 다음으로 첨부 accessor 역할을 봅시다 이것은 변수와 아래 첨자에 첨부될 수 있고 여기에 accessor를 설치할 수 있습니다 'get'이나 'set', 'willSet' 'didSet' 같은 것을요 이걸 어떻게 쓸 수 있을까요? 딕셔너리를 둘러싸고 있으며 프로퍼티로 콘텐츠에 접근하게 하는 유형이 여러 개 있다고 가정합시다 예를 들어 이 'Person' 구조체는 'name'과 'height', 'birth_date' 필드에 접근하게 하는데 만일 이 필드 세 개 이외에 딕셔너리에 다른 정보가 있다면 프로그램에서 그냥 보존하거나 무시하겠죠 이 프로퍼티 세 개는 산출된 getter와 setter가 필요하지만 이를 직접 작성하는 건 지루한 일인 데다 프로퍼티 래퍼는 함께 사용되는 유형에 저장된 다른 프로퍼티에 접근할 수 없기에 이 래퍼를 쓸 수도 없습니다 그러니 이 문제를 도울 수 있는 첨부 accessor 매크로를 작성하죠 이걸 'DictionaryStorage'라고 부르고 여기에 'key' 매개변수를 주는데 바로 딕셔너리가 'birth_date'를 _ 기호와 함께 쓰기 때문이죠 또한 이 key를 뺄 수도 있고 그럼 nil로 기본값이 될 겁니다 그러면 매크로가 프로퍼티의 이름을 key로 쓰겠죠 이제부터 커다란 accessor 블록을 작성하는 대신 각 프로퍼티 앞마다 '@DictionaryStorage'를 붙이면 매크로가 accessor를 생성해 줄 겁니다 멋지게 개선하기는 했지만 여전히 보일러플레이트가 좀 있죠 동일한 DictionaryStorage 속성이 반복되네요 많지는 않아도 여전히 보일러플레이트입니다 일부 내장 속성을 전체 유형이나 확장에 적용해 이런 종류의 상황에 대처할 수 있습니다 '첨부 멤버 속성' 역할은 여러분의 매크로도 그렇게 작동하게 합니다 매크로는 유형이나 확장에 첨부되어 첨부된 게 무엇이든 그 멤버에 속성을 추가할 수 있죠 어떻게 작동하는지 봅시다 여기서는 약간 다른 걸 할 겁니다 새 매크로를 선언하는 대신 DictionaryStorage 매크로에 다른 역할 속성을 추가하는 거죠 이미 존재하는 첨부 accessor 역할 옆에요 이는 매크로 생성에 매우 유용한 기술입니다 독립형 역할 두 개를 빼면 어떤 조합의 역할이든 구성할 수 있죠 Swift가 어떤 걸 써야 할지 모르는 부분이 있거든요 Swift는 타당한 모든 역할을 그 역할을 적용한 곳에 전개하겠지만 적어도 그 역할 중 하나는 작동해야 하죠 그래서 DictionaryStorage를 유형에 첨부하면 Swift는 '멤버 속성' 역할을 전개할 겁니다 이를 프로퍼티에 첨부하면 Swift는 accessor 역할을 전개하겠죠 하지만 이를 함수에 첨부하면 컴파일 오류가 뜰 텐데 왜냐하면 DictionaryStorage에는 함수에 첨부할 수 있는 역할이 없기 때문이죠
모든 프로퍼티에 일일이 첨부하는 대신 DictionaryStorage에 이 두 번째 역할을 추가하면 저절로 전체 유형에 첨부할 수 있습니다 이니셜라이저나 dictionary 프로퍼티 및 birth_date처럼 DictionaryStorage 속성을 이미 가진 프로퍼티 등 특정 멤버를 건너뛸 수 있는 로직이 매크로에 생길 겁니다 하지만 이것은 DictionaryStorage 속성을 다른 저장된 프로퍼티에 추가할 테고 그 후 이 속성들은 앞서 보았던 accessor에 전개될 겁니다 어느 정도 나아졌지만 아직도 없앨 수 있는 보일러플레이트가 있죠 바로 이니셜라이저와 저장 프로퍼티입니다 이들은 'DictionaryRepresentable' 프로토콜이 요구하며 프로퍼티는 accessor가 사용하게 되지만 이들은 DictionaryStorage를 쓰는 모든 유형에서 정확히 똑같죠 DictionaryStorage 매크로가 이를 저절로 추가하게 하여 직접 작성할 필요가 없게 해보죠 첨부 멤버 역할을 이용해 이렇게 할 수 있는데요 멤버 속성 매크로처럼 이 매크로는 유형과 확장에 적용할 수 있지만 속성을 기존 멤버에 추가하는 대신 완전히 새로운 멤버를 추가하게 됩니다 따라서 메서드와 프로퍼티 이니셜라이저 등을 추가할 수 있죠 심지어 저장 프로퍼티를 클래스와 구조체에 또는 케이스를 열거형에 추가할 수 있어요 다시금 새로운 첨부 멤버 역할을 DictionaryStorage 매크로에 더해 다른 두 개와 함께 구성할 겁니다 이 새로운 역할은 이니셜라이저와 dictionary라는 프로퍼티를 추가할 겁니다
아마 지금쯤 궁금하시겠죠 두 가지 다른 매크로가 같은 코드에 적용되면 무엇이 먼저 전개될까요? 정답은 바로 어느 쪽도 상관이 없다는 겁니다 각각은 다른 쪽이 제공하는 전개식 없이 선언의 처음 버전을 볼 겁니다 따라서 순서를 걱정할 필요가 없죠 컴파일러가 언제 매크로를 전개하든 똑같은 결과를 볼 겁니다 첨부 멤버 역할을 추가하면 이 두 가지 멤버를 작성하지 않아도 됩니다 그저 DictionaryStorage를 유형에 이용하는 것만으로 저절로 멤버를 추가해 줄 테니까요 그러면 다른 역할이 DictionaryStorage 속성을 프로퍼티에 추가할 테고 이 속성들은 accessor로 전개되는 등의 일이 일어납니다
하지만 아직 제거할 마지막 보일러플레이트가 하나 남았는데요 DictionaryRepresentable 프로토콜의 준수입니다 첨부된 준수 역할은 이 일에 꼭 맞죠 이 역할은 유형이나 확장에 준수를 추가할 수 있습니다 마지막 첨부된 준수 역할 한 가지를 DictionaryStorage 매크로에 추가해 다른 세 개와 함께 구성하죠 이 새 역할은 준수를 'Dictionary Representation'에 추가할 겁니다 이제는 준수를 수작업으로 작성하지 않아도 됩니다 DictionaryStorage 속성은 이미 accessor를 위해 추가했고 생성된 멤버가 자동으로 준수 또한 추가할 겁니다 이미 하고 있던 다른 일과 함께요 시작점을 본 지 꽤 오래됐으니 다시 상기시켜 드릴게요 우리는 반복되는 코드로 가득한 크고 다루기 힘든 유형을 가져다가 그 코드 대부분을 매우 강력한 매크로의 여러 역할로 옮겼습니다 따라서 남은 것은 간결하게 이 유형에서 특별한 것만 지정하게 되었죠 DictionaryStorage를 쓸 수 있는 유형 10개나 20개가 있다고 상상해 보세요 이 많은 걸 다 작업하는 게 얼마나 더 쉬워질까요? 선언과 역할에 관해 얘기하느라 오랜 시간을 보냈는데요 아직은 이들이 전개된 코드가 마법처럼 나타난 것 같이 보입니다 이 공백을 채워서 어떻게 매크로를 실행할지 얘기해 봅시다 지금까지 매크로 선언을 보여드릴 때는 매우 중요한 무언가를 빠뜨렸는데 바로 구현입니다 구현은 = 기호 뒤에서 항상 다른 매크로 안에 들어가죠 때로는 여러분이 작성한 다른 매크로가 재배열된 매개변수와 함께 나오거나 리터럴로 지정된 추가 매개변수와 함께 나옵니다 하지만 대개는 외부 매크로를 쓸 겁니다 외부 매크로란 컴파일러 플러그인으로 구현되는 것인데요 앞에서 얘기한 컴파일러 플러그인을 기억하실지 모르겠네요 컴파일러에서 매크로 사용을 인식하면 별도의 프로세스에서 플러그인을 작동하고 매크로를 전개하게 요청한다고 했죠 '#externalMacro'는 그 관계를 정의합니다 이것은 컴파일러가 실행해야 하는 플러그인과 그 플러그인 속의 유형 이름을 지정하죠 따라서 Swift가 이 매크로를 전개할 때 'MyLibMacros'라는 플러그인을 실행하고 이를 전개하기 위해 'Stringify Macro'라는 유형을 요청할 겁니다 그럼 매크로 선언이 다른 API와 함께 여러분의 평소 라이브러리에 들어가지만 매크로 구현은 별도의 컴파일러 플러그인 모듈에 들어갑니다 그러면 #externalMacro가 선언과 이를 구현하는 유형 사이의 링크를 만들죠 매크로 구현은 어떻게 보일까요? DictionaryStorage가 어떻게 구현되었을지 함께 봅시다 앞서 DictionaryStorage 매크로는 저장 프로퍼티와 이니셜라이저를 유형에 추가하는 첨부 멤버 역할이 있었죠 여기에 그 역할의 간단한 구현이 있습니다 한 번에 한 단계씩 보고 그 작동법을 알아보죠 맨 위에서 SwiftSyntax라는 라이브러리를 불러오며 시작합니다 SwiftSyntax는 Swift 프로젝트가 유지하는 패키지로서 소스 코드의 파싱 및 점검 조종, 생성을 돕습니다 Swift 컨트리뷰터는 언어가 발전할 때마다 SwiftSyntax를 최신으로 유지하여 Swift 컴파일러가 지원하는 모든 기능을 지원합니다 SwiftSyntax는 소스 코드를 특별한 트리 구조로 표현하는데요 이를테면 이 코드 샘플의 Person 구조체는 'StructDeclSyntax'라는 유형의 인스턴스로서 대표되죠 하지만 이 인스턴스에는 프로퍼티가 있고 그 프로퍼티 각각은 구조체 선언의 일부를 나타냅니다 속성의 목록은 'attributes' 프로퍼티 안에 있습니다 실제 키워드인 'struct'는 'structKeyword' 프로퍼티 안에 있고요 이 구조체의 이름은 'identifier' 프로퍼티 안에 있습니다 그리고 중괄호가 있는 본문과 구조체의 멤버는 'memberBlock' 프로퍼티 안에 있습니다 'modifier'처럼 일부 구조체 선언이 가진 것들을 나타내는 프로퍼티도 있죠 이건 아무것도 없는데 이를 nil이라 합니다 이 프로퍼티의 일부 구문 노드는 '토큰'이라고 불리는데 토큰은 소스 파일 속의 특정 텍스트를 나타냅니다 이름이나 키워드 구두점 일부 등이죠 토큰은 그 텍스트와 공백이나 주석 같은 다른 주변 정보를 포함합니다 구문 트리를 충분히 자세히 들여다보면 소스 코드의 모든 바이트를 다루는 토큰 노드가 보일 겁니다 하지만 attributes 프로퍼티의 'AttributeListSyntax' 노드나 'memberBlock' 프로퍼티의 'MemberDeclBlockSyntax' 노드는 토큰이 아닙니다 이것들은 자체 프로퍼티 안에 자식 노드가 있죠 예를 들어 memberBlock 프로퍼티 안을 보면 중괄호를 여는 토큰과 함께 멤버 목록에 대한 MemberDeclListSyntax 노드와 중괄호를 닫는 토큰이 있을 겁니다 MemberDeclListSyntax 노드의 콘텐츠를 계속 탐색한다면 결국 각 프로퍼티에 대한 노드 등을 찾게 될 겁니다 SwiftSyntax로 작업하는 건 그 자체로 광범위한 주제입니다 그래서 이 영상을 두 배로 길게 만드는 대신 다른 자료 두 개를 참고로 알려드릴게요 하나는 본 세션과 짝이 되는 'Swift 매크로 작성하기' 세션으로 어떻게 특정 소스 코드 조각이 구문 트리로 표현되는지 알아내기 위한 실용적 팁을 포함하고요 다른 하나는 SwiftSyntax 패키지의 문서입니다 인터넷에서 찾을 수 있고 또는 만일 Xcode의 Build Documentation 커맨드를 매크로 패키지에서 쓴다면 SwiftSyntax 문서가 개발자 문서 창에 나타날 겁니다 주요 SwiftSyntax 라이브러리 외에도 저희는 또 다른 모듈 두 개를 가져올 건데요 하나는 'SwiftSyntaxMacros'로 매크로 작성에 필요한 프로토콜과 유형을 제공합니다 다른 건 'SwiftSyntaxBuilder'입니다 이 라이브러리는 새로 생성된 코드를 표현하기 위한 구문 트리의 구성에 쓰는 편의 API를 제공하죠 이게 없어도 매크로를 작성할 수 있지만 정말 편리하기 때문에 한번 이용해 보시는 걸 적극 추천합니다 이 라이브러리들을 가져왔으니 플러그인이 제공해야 하는 'DictionaryStorageMacro' 유형을 실제로 작성해 보죠 이것이 'MemberMacro'라 불리는 프로토콜을 준수하는 걸 보세요 각 역할에는 그에 상응하는 프로토콜이 있으며 구현은 매크로가 제공하는 각 역할에 대한 프로토콜을 준수해야 합니다 DictionaryStorage 매크로는 이런 역할이 네 개가 있는데 따라서 DictionaryStorageMacro 유형은 상응하는 프로토콜 네 개를 준수해야 할 겁니다 하지만 간단한 설명을 위해 지금은 MemberMacro 준수 하나만 걱정하도록 하죠 이 유형의 본문으로 가면 메서드로 'expansion of'와 'providingMembersOf' 및 'in'이 보이죠 이 메서드는 MemberMacro 프로토콜이 요구하며 Swift 컴파일러가 매크로 사용 시 멤버 역할을 전개하려고 호출하는 것입니다 인수는 아직 쓰지 않고 있지만 여기에 대해서도 나중에 얘기하죠 지금은 이것이 정적 메서드라는 것만 보세요 모든 전개 메서드는 정적이기에 DictionaryStorageMacro 유형의 인스턴스를 Swift가 생성하지 않습니다 그저 이를 메서드의 컨테이너로 사용하죠 각 전개 메서드는 소스 코드에 삽입된 SwiftSyntax 노드를 반환합니다 멤버 매크로는 유형에 멤버로서 추가할 선언 목록으로 전개되므로 멤버 매크로에 대한 전개 메서드는 'DeclSyntax' 노드 어레이를 반환합니다 본문을 들여다보면 어레이가 생성되는 게 보이죠 이 어레이에는 이니셜라이저와 이 매크로가 추가하길 원하는 저장 프로퍼티가 있습니다 여기 있는 'var dictionary'가 평범한 문자열처럼 보이지만 사실 그렇지 않습니다 이 문자열 리터럴은 DeclSyntax가 예상되는 곳에 작성되고 있으며 그래서 Swift는 이를 소스 코드 조각처럼 처리하고 Swift 파서에 DeclSyntax 노드로 바꾸라고 요청합니다 SwiftSyntaxBuilder 라이브러리의 편의 기능 중 하나를 보았습니다 이걸 일찍 가져와서 다행이죠 방금의 실행 내용과 다른 세 역할에 대한 프로토콜 준수가 있으면 DictionaryStorage 매크로의 구현이 작동할 겁니다 이걸 올바르게 쓰면 매크로가 잘 작동하겠지만 잘못 사용하면 어떻게 될까요? 예를 들어 이걸 구조체 대신 열거형에 적용한다면요? 첨부 멤버 역할은 저장 dictionary 프로퍼티를 추가하려고 할 겁니다 열거형은 저장 프로퍼티를 가질 수 없으니 Swift에서 오류가 발생하겠죠 '열거형은 저장 프로퍼티를 포함하면 안 된다'라는 오류겠죠 Swift에서 이 코드의 컴파일을 막는 것은 좋지만 오류 메시지가 약간 혼란스럽지 않나요? 왜 DictionaryStorage 매크로가 저장 프로퍼티를 생성하려고 했는지나 어떤 걸 달리해야 했는지가 불분명하죠 앞서 본 Swift의 목표 하나는 매크로가 입력 속 잘못을 감지하고 커스텀 오류 메시지를 띄워야 한다는 거였습니다 그러니 매크로의 구현을 수정해서 여기에 대해 훨씬 더 명확한 오류 메시지를 만들게 해 봅시다 '@DictionaryStorage는 구조체에만 적용될 수 있다' 이 메시지는 개발자에게 무엇을 잘못했는지 더 잘 알려줄 겁니다 이를 성공하는 열쇠는 전개 메서드의 매개변수인데 지금까지는 이를 무시했죠 정확한 인수는 역할마다 살짝 다른데요 멤버 매크로에는 인수가 세 개 있습니다 첫 번째는 attribute라 불리고 그 유형은 AttributeSyntax죠 이것은 실제 DictionaryStorage 속성으로 개발자가 매크로를 쓰려고 작성한 겁니다 두 번째 인수는 'declaration'이라 불리고 'DeclGroupSyntax'를 준수하는 유형이죠 DeclGroupSyntax는 구조체와 열거형, 클래스, 액터 프로토콜 및 확장에 대한 노드가 모두 준수하는 프로토콜입니다 이 매개변수는 개발자가 속성을 첨부한 선언을 우리에게 줍니다 마지막 매개변수는 'context'로 'MacroExpansionContext'를 준수하는 유형에 속합니다 context 객체는 매크로 구현이 컴파일러와 소통하길 원할 때 사용됩니다 오류와 경고 메시지 발신을 비롯해 몇 가지 일을 할 수 있죠 이 매개변수 세 개를 모두 써서 오류 메시지를 띄울 겁니다 어떻게 하는지 보죠 첫째로는 문제를 감지해야 합니다 declaration 매개변수 유형을 확인하여 이 일을 할 겁니다 선언 각각은 다른 유형을 갖게 되죠 그래서 만일 구조체라면 그 유형은 'StructDeclSyntax'가 됩니다 만약 열거형이라면 'EnumDeclSyntax'가 되는 식이죠 그러니 declaration 매개변수의 'is' 메서드를 호출하고 StructDeclSyntax를 전달하는 guard-else문을 작성할 겁니다 만일 선언이 구조체가 아니라면 'else' 블록으로 가게 되겠죠 현재로선 빈 어레이를 반환하므로 매크로가 프로젝트에 코드를 더하지 않지만, 정말 하고 싶은 건 오류 메시지 발신이죠 쉬운 방법은 평범한 Swift 오류를 발생시키는 거지만 그렇게 하면 여러분이 출력을 통제할 수 없습니다 그러니 대신 더 정교한 오류를 생성하는 좀 더 복잡한 방법을 보여드릴게요 첫 단계는 'Diagnostic'이라는 유형의 인스턴스를 만드는 건데요 약간은 컴파일러 전문 용어 같죠 의사가 부러진 다리의 엑스레이를 보고 골절 진단을 내리듯이 컴파일러나 매크로가 여러분의 깨진 코드 구문 트리를 보고 오류나 경고를 진단합니다 그래서 오류 표시 인스턴스를 Diagnostic이라 부르죠 Diagnostic은 적어도 두 가지 정보를 담는데 첫째는 오류가 일어난 구문 노드이고 따라서 컴파일러에서 어떤 줄이 틀렸는지 표시할 곳을 알 수 있죠 여기에서는 사용자가 작성한 DictionaryStorage 속성을 가리키려고 하는데요 이건 다행히 메서드가 전달한 attribute 매개변수가 제공하죠 둘째는 컴파일러가 만들길 바라는 실제 메시지입니다 커스텀 유형을 만들고 인스턴스를 전달하여 이 메시지를 제공하죠 이걸 간략하게 봅시다 'MyLibDiagnostic' 유형은 이 모듈이 생성할 수 있는 모든 진단을 정의하죠 저희는 열거형을 사용하여 각 진단에 대한 케이스를 제공하기로 했지만 원하면 다른 유형을 이용할 수도 있습니다 이 유형은 일종의 던질 수 있는 Swift 오류처럼 작동하죠 이것은 'DiagnosticMessage' 프로토콜을 준수하며 진단에 대한 정보를 제공하는 프로퍼티를 잔뜩 갖고 있습니다 그중에서도 'severity' 프로퍼티가 중요한데요 이건 진단 내용이 오류인지 경고인지를 명시합니다
그리고 'message' 프로퍼티는 실제 오류 메시지와 'diagnosticID' 프로퍼티를 생성합니다 여러분은 플러그인의 모듈명을 도메인으로 일종의 독특한 문자열을 ID로 사용해야 합니다 저는 이 열거형에 rawValue 문자열을 쓰기로 했지만 그냥 편의상의 선택입니다 메시지를 손에 넣었으니 이제 진단을 내릴 수 있습니다 다음은 콘텍스트에 진단을 내리라고 하면 끝입니다
꽤 기본적인 진단이지만 만일 원한다면 훨씬 더 화려하게 할 수도 있죠 예를 들어 Xcode의 Fix 버튼으로 자동 적용 되는 Fix-It을 진단에 추가할 수도 있고요 하이라이트를 추가하고 코드의 다른 위치를 가리키는 메모를 첨부할 수도 있죠 정말로 개발자들에게 최상급 오류 알림 경험을 줄 수 있어요 매크로가 올바르게 적용되는지 확인하고 나서도 여전히 실제로 전개식을 만들어야 하죠 SwiftSyntax는 이를 위한 몇 가지 도구를 줍니다 구문 노드는 변하지 않지만 새 노드를 만들거나 기존 노드의 수정된 버전을 반환하는 API가 많이 있죠 SwiftSyntaxBuilder 라이브러리는 SwiftUI 스타일의 구문 빌더를 자식 노드 일부가 따라오는 클로저에 의해 지정되는 곳에 추가합니다 예를 들어 다차원적 어레이 매크로는 구문 빌더를 사용하여 생성하는 유형에 맞는 매개변수가 몇 개든 만들 수 있습니다 DictionaryStorage 프로퍼티와 이니셜라이저를 만들 때 쓴 문자열 리터럴 기능도 보간법을 지원하죠
이 모든 기능은 다양한 상황에서 유용합니다 여러분은 아마 특히 복잡한 매크로에 이 몇 개를 조합하게 되실 겁니다 하지만 문자열 리터럴 기능은 특히 대량의 코드를 위한 구문 트리를 생성하는 데 유용한데요 그 보간 기능에 대해 배울 것이 조금 있습니다 코드를 생성하기 위해 이 기능을 쓰는 법을 알아봅시다 앞서 unwrap 매크로를 말씀드렸는데요 선택적 값과 메시지 문자열을 가져가서 클로저에 둘러싸인 guard let으로 전개된다고 했죠 이 코드의 전반적 모양은 항상 똑같을 테지만 많은 콘텐츠가 특정 사용 위치에 맞춰져 있습니다 guard let문에 집중하여 이 문만을 생성하는 함수의 작성법을 알아봅시다 먼저 방금 본 그 코드 샘플을 가져다가 Statement Syntax 노드를 반환하는 'makeGuardStatement'라는 헬퍼 메서드에 놓죠 다음엔 바뀌어야 하는 모든 걸 교체하기 위해 사용 위치에 따라 보간을 서서히 추가합니다 제일 먼저 하는 일은 올바른 메시지 문자열을 추가하는 건데요 메시지 문자열은 임의 표현식으로 이걸 ExprSyntax 노드로서 전달하고 보간을 넣습니다 이런 평범한 보간으로 구문 노드를 코드에 추가할 수 있지만 일반 문자열은 추가할 수 없습니다 이는 무효한 코드를 실수로 삽입하는 걸 방지하는 안전 기능입니다 guard-let 조건문도 변수명이라는 걸 빼면 비슷합니다 그래서 이건 토큰이지 표현식이 아니죠 어쨌든 TokenSyntax 매개변수를 추가하고 이를 보간합니다 표현식을 보간했듯이요 오류 메시지에 언래핑되는 표현식을 추가할 때는 더 복잡해지는데요 매크로의 기능 중 하나는 이게 실패할 경우 언래핑하려고 했던 코드를 출력한다는 겁니다 즉, 구문 노드의 문자열화된 버전을 포함한 문자열 리터럴을 만들어야 한다는 겁니다
Statement Syntax 리터럴에서 접두사를 끌어내어 일반 문자열인 변수에 넣는 걸로 시작하죠 이 문자열을 보간하겠지만 'literal:'으로 시작하는 특별한 보간을 사용할 겁니다 이때 SwiftSyntax는 문자열의 내용을 문자열 리터럴로서 추가할 겁니다 이것은 또한 매크로와 숫자 불리언, 어레이 딕셔너리 및 기타 옵셔널에서 산출한 다른 정보에서 리터럴을 만들 때도 작동합니다 이제 변수 안에 문자열을 구축하고 있으므로 메시지에 올바른 코드가 있도록 이를 바꿀 수 있습니다 원래의 표현식에 대한 매개변수를 추가하고 이것의 'description' 프로퍼티를 문자열에 보간하세요 이스케이핑을 위해 특별한 작업을 할 필요는 없습니다 literal: 보간은 문자열이 특수 문자를 포함할 때 저절로 감지하여 이스케이핑을 추가하거나 코드의 유효성을 위해 원시 리터럴로 전환할 겁니다 이렇게 literal: 보간은 일을 매우 수월히 처리하게 도와주죠 마지막으로 처리할 것은 파일과 줄 번호입니다 이건 약간 까다로운데요 바로 컴파일러가 매크로에 전개되는 소스 위치를 말해주지 않기 때문입니다 하지만 매크로 전개식 콘텍스트에는 컴파일러가 소스 위치 정보를 가진 리터럴로 바뀌는 특수 구문 노드를 생성하기 위해 쓰는 API가 있죠 이제 어떻게 하는지 살펴봅시다 매크로 전개식 콘텍스트에 대한 인수를 하나 추가하고 'location of' 메서드를 쓸 겁니다 그러면 여러분이 제공하는 노드가 무엇이든 그 위치에 대한 구문 노드를 생성하는 객체가 반환되죠 만일 노드가 컴파일러가 전달한 것이 아니라 매크로가 만든 것이라면 nil이 반환될 겁니다 하지만 'originalWrapped'는 사용자가 작성한 인수 중 하나라는 걸 알죠 따라서 그 위치는 절대 nil이 되지 않을 것이고 안전하게 결과를 강제로 언래핑할 수 있습니다 이제는 파일과 줄 번호를 위한 구문 노드를 보간하기만 하면 끝입니다 이제 올바른 'guard'문을 생성하고 있죠 지금까지 어떻게 매크로를 작동할지를 얘기했는데요 이제는 매크로를 잘 작동할 방법을 얘기해 봅시다 먼저 이름 충돌부터 살펴보죠 앞서 unwrap 매크로를 볼 때는 간단한 변수명을 언래핑하는 예시를 살펴봤습니다 하지만 만일 더 복잡한 표현식을 언래핑하려 한다면 매크로가 다르게 전개돼야 합니다 이것은 표현식의 결과를 캡처하여 'wrappedValue' 변수에 넣고 이를 언래핑하는 코드를 생성합니다
그런데 만일 wrappedValue 변수를 메시지에서 쓰려고 하면 어떻게 될까요? 컴파일러가 wrappedValue를 찾아 나서면 이에 가까운 걸 찾는 걸로 끝날 겁니다 그래서 실제로 의도한 것 대신 찾은 걸 쓰게 되죠
사용자가 우연히라도 쓰지 않을 이름을 고르는 것으로 이 문제를 고칠 수 있겠지만 아예 그런 우연이 없도록 하면 더 낫지 않을까요?
Macro Expansion Context의 'makeUniqueName' 메서드가 바로 그런 일을 합니다 이 메서드는 사용자 코드나 다른 매크로 전개식에서 절대 쓰지 않을 변수명을 반환하죠 따라서 메시지 문자열이 실수로 이를 참조하지 않을 거라고 확신할 수 있죠 몇 분은 궁금해하실지도 모릅니다 왜 Swift에서 자동으로 이 일을 막지 않는 걸까요? 일부 언어는 소위 '위생적인' 매크로 시스템이 있어서 여기선 매크로 안의 이름이 외부 이름과 구분되어 서로 충돌하지 않습니다
Swift는 그렇지 않은데 그 이유는 많은 매크로에서 외부 이름을 쓸 필요가 있다는 걸 알게 됐기 때문이죠 DictionaryStorage 매크로를 생각해 보세요 이건 dictionary 프로퍼티를 유형에 쓰죠 만일 매크로 내의 dictionary가 외부의 dictionary와 다른 걸 의미한다면 이를 작동하기가 꽤 힘들 겁니다
또 때때로 매크로가 아닌 코드가 접근할 수 있는 완전히 새로운 이름을 도입하고 싶을 때도 있죠 peer 매크로와 member 매크로 declaration 매크로는 이 기능을 위해 존재하는 거나 마찬가지입니다 하지만 그럴 경우 이 매크로들은 추가하는 이름을 선언하여 컴파일러가 알 수 있게 해야 하죠 이 선언은 역할 속성 안에서 이루어집니다 전에는 눈치채지 못했을지도 모르지만 사실 지금껏 내내 이 선언들을 목격했죠 DictionaryStorage 매크로의 member 역할은 dictionary와 'init' 이름을 지정한 'names:' 매개변수를 갖고 있었죠 사실 지금까지 본 매크로 대부분에 names 인수를 가진 역할이 적어도 하나는 있었습니다
쓸 수 있는 이름 지정자가 다섯 개 있는데요 'overloaded'는 어떤 것에 매크로가 첨부되었든 이와 똑같은 기본 이름을 가진 선언을 매크로가 추가한다는 뜻입니다 'prefixed'는 매크로가 같은 기본 이름을 가진 선언을 추가하되 지정된 접두사가 추가되었다는 뜻이고요 'suffixed'도 앞과 동일하지만 접두사 대신 접미사가 붙죠 'named'는 매크로가 구체적이고 고정된 기본 이름을 가진 선언을 추가한다는 뜻이며 'arbitrary'는 매크로가 앞의 규칙 어떤 걸로도 묘사할 수 없는 기타 이름을 가진 선언을 추가한다는 뜻입니다 arbitrary 지정자를 정말 흔하게 쓴답니다 예를 들어 다차원 어레이 매크로는 그 매개변수 중 하나에서 산출된 이름을 가진 유형을 선언하기 때문에 arbitrary를 지정해야 하죠 하지만 다른 지정자 중 하나를 쓸 수 있다면 이를 권장합니다 컴파일러와 코드 완성 같은 다른 도구를 더 빠르게 할 겁니다 이제 여기까지 왔으니 모두 첫 매크로를 작성하고 싶어 몸이 근질거릴 겁니다 어떻게 시작할지 좋은 생각이 있을지도 모르고요 언제 전개됐는지 시간과 날짜를 삽입하는 매크로를 작성해 보세요 좋은 생각이겠죠? 틀렸습니다 사실 이런 매크로는 절대 작성하면 안 됩니다 그 이유를 설명해 드리죠 매크로는 오로지 컴파일러가 제공하는 정보만 써야 합니다 컴파일러에서는 매크로 구현이 순수한 함수라고 가정하며 만일 컴파일러가 제공한 데이터가 바뀌지 않았다면 전개식도 바뀔 수 없다고 가정합니다 이를 우회한다면 모순된 동작을 보게 될지도 모릅니다 매크로 시스템은 이 규칙을 위반할 수 있는 일부 동작을 막도록 디자인되었죠 컴파일러 플러그인은 매크로구현이 디스크의 파일을 읽거나 네트워크에 접근하는 걸 막는 샌드박스에서 실행됩니다 하지만 샌드박스는 모든 나쁜 동작을 막지는 않습니다 날짜나 무작위 숫자 같은 정보를 얻기 위해 API를 쓸 수도 있고 전역 변수의 전개식 하나로부터 정보를 저장해 다른 전개에 쓸 수도 있죠 하지만 이런 일을 한다면 매크로가 오작동할 수도 있습니다 그러니 하지 마세요 이제 마지막으로 매우 중요한 테스트에 관해 얘기합시다 매크로 플러그인은 그저 평범한 Swift 모듈이고 여러분은 이를 위해 평범한 단위 테스트를 작성할 의무가 있죠 테스트 중심 개발은 Swift 매크로를 개발하는 데 대단히 효과적인 접근법입니다 SwiftSyntaxMacrosTestSupport의 'assertMacroExpansion' 헬퍼는 매크로가 올바른 전개식을 생성하는지 확인할 겁니다 그저 매크로의 예시와 전개돼야 하는 코드를 주기만 하면 이들이 서로 일치하게 할 겁니다 오늘 Swift 매크로에 대해 많은 걸 배웠는데요 매크로는 작은 사용 위치를 더 복잡한 코드 조각으로 전개하는 새 언어 기능을 디자인해 보일러플레이트를 줄이게 합니다 보통은 라이브러리에서 매크로를 다른 API와 함께 선언할 수도 있죠 하지만 여러분은 실제로 Swift 코드를 보안 샌드박스에서 실행하는 별도의 플러그인에서 구현합니다 매크로의 역할은 이것의 사용처와 그 전개식이 어떻게 나머지 프로그램에 통합되는지 표현하죠 그리고 여러분은 매크로를 위한 단위 테스트를 반드시 작성하여 예상대로 작동하는지 확인해야 합니다 아직 시청하지 않으셨다면 'Swift 매크로 작성하기' 세션을 다음 영상으로 보시기 바랍니다 이 영상에서 Xcode의 매크로 개발 도구 및 매크로 패키지 템플릿 사용법과 SwiftSyntax 트리를 검사하고 정보를 추출하는 방법 단위 테스트 주변의 매크로 개발 워크플로를 구축하는 법을 확인하세요 시청해 주셔서 감사합니다 즐거운 코딩하세요 ♪ ♪
-
-
0:44 - The #unwrap expression macro, with a more complicated argument
let image = #unwrap(request.downloadedImage, message: "was already checked") // Begin expansion for "#unwrap" { [wrappedValue = request.downloadedImage] in guard let wrappedValue else { preconditionFailure( "Unexpectedly found nil: ‘request.downloadedImage’ " + "was already checked", file: "main/ImageLoader.swift", line: 42 ) } return wrappedValue }() // End expansion for "#unwrap"
-
0:50 - Existing features using expansions (1)
struct Smoothie: Codable { var id, title, description: String var measuredIngredients: [MeasuredIngredient] static let berryBlue = Smoothie(id: "berry-blue", title: "Berry Blue") { """ Filling and refreshing, this smoothie \ will fill you with joy! """ Ingredient.orange .measured(with: .cups).scaled(by: 1.5) Ingredient.blueberry .measured(with: .cups) Ingredient.avocado .measured(with: .cups).scaled(by: 0.2) } }
-
1:11 - Existing features using expansions (2)
struct Smoothie: Codable { var id, title, description: String var measuredIngredients: [MeasuredIngredient] // Begin expansion for Codable private enum CodingKeys: String, CodingKey { case id, title, description, measuredIngredients } init(from decoder: Decoder) throws { … } func encode(to encoder Encoder) throws { … } // End expansion for Codable static let berryBlue = Smoothie(id: "berry-blue", title: "Berry Blue") { """ Filling and refreshing, this smoothie \ will fill you with joy! """ Ingredient.orange .measured(with: .cups).scaled(by: 1.5) Ingredient.blueberry .measured(with: .cups) Ingredient.avocado .measured(with: .cups).scaled(by: 0.2) } }
-
3:16 - Macros inputs are complete, type-checked, and validated
#unwrap(1 + ) // error: expected expression after operator @AddCompletionHandler(parameterName: 42) // error: cannot convert argument of type 'Int' to expected type 'String' func sendRequest() async throws -> Response @DictionaryStorage class Options { … } // error: '@DictionaryStorage' can only be applied to a 'struct'
-
3:45 - Macro expansions are inserted in predictable ways
func doThingy() { startDoingThingy() #someUnknownMacro() finishDoingThingy() }
-
4:51 - How macros work, featuring #stringify
func printAdd(_ a: Int, _ b: Int) { let (result, str) = #stringify(a + b) // Begin expansion for "#stringify" (a + b, "a + b") // End expansion for "#stringify" print("\(str) = \(result)") } printAdd(1, 2) // prints "a + b = 3"
-
5:43 - Macro declaration for #stringify
/// Creates a tuple containing both the result of `expr` and its source code represented as a /// `String`. @freestanding(expression) macro stringify<T>(_ expr: T) -> (T, String)
-
7:11 - What’s an expression?
let numPixels = (x + width) * (y + height) // ^~~~~~~~~~~~~~~~~~~~~~~~~~ This is an expression // ^~~~~~~~~ But so is this // ^~~~~ And this
-
7:34 - The #unwrap expression macro: motivation
// Some teams are nervous about this: let image = downloadedImage! // Alternatives are super wordy: guard let image = downloadedImage else { preconditionFailure("Unexpectedly found nil: downloadedImage was already checked") }
-
8:03 - The #unwrap expression macro: macro declaration
/// Force-unwraps the optional value passed to `expr`. /// - Parameter message: Failure message, followed by `expr` in single quotes @freestanding(expression) macro unwrap<Wrapped>(_ expr: Wrapped?, message: String) -> Wrapped
-
8:21 - The #unwrap expression macro: usage
let image = #unwrap(downloadedImage, message: "was already checked") // Begin expansion for "#unwrap" { [downloadedImage] in guard let downloadedImage else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + "was already checked", file: "main/ImageLoader.swift", line: 42 ) } return downloadedImage }() // End expansion for "#unwrap"
-
9:09 - The #makeArrayND declaration macro: motivation
public struct Array2D<Element>: Collection { public struct Index: Hashable, Comparable { var storageIndex: Int } var storage: [Element] var width1: Int public func makeIndex(_ i0: Int, _ i1: Int) -> Index { Index(storageIndex: i0 * width1 + i1) } public subscript (_ i0: Int, _ i1: Int) -> Element { get { self[makeIndex(i0, i1)] } set { self[makeIndex(i0, i1)] = newValue } } public subscript (_ i: Index) -> Element { get { storage[i.storageIndex] } set { storage[i.storageIndex] = newValue } } // Note: Omitted additional members needed for 'Collection' conformance } public struct Array3D<Element>: Collection { public struct Index: Hashable, Comparable { var storageIndex: Int } var storage: [Element] var width1, width2: Int public func makeIndex(_ i0: Int, _ i1: Int, _ i2: Int) -> Index { Index(storageIndex: (i0 * width1 + i1) * width2 + i2) } public subscript (_ i0: Int, _ i1: Int, _ i2: Int) -> Element { get { self[makeIndex(i0, i1, i2)] } set { self[makeIndex(i0, i1, i2)] = newValue } } public subscript (_ i: Index) -> Element { get { storage[i.storageIndex] } set { storage[i.storageIndex] = newValue } } // Note: Omitted additional members needed for 'Collection' conformance }
-
10:03 - The #makeArrayND declaration macro: macro declaration
/// Declares an `n`-dimensional array type named `Array<n>D`. /// - Parameter n: The number of dimensions in the array. @freestanding(declaration, names: arbitrary) macro makeArrayND(n: Int)
-
10:15 - The #makeArrayND declaration macro: usage
#makeArrayND(n: 2) // Begin expansion for "#makeArrayND" public struct Array2D<Element>: Collection { public struct Index: Hashable, Comparable { var storageIndex: Int } var storage: [Element] var width1: Int public func makeIndex(_ i0: Int, _ i1: Int) -> Index { Index(storageIndex: i0 * width1 + i1) } public subscript (_ i0: Int, _ i1: Int) -> Element { get { self[makeIndex(i0, i1)] } set { self[makeIndex(i0, i1)] = newValue } } public subscript (_ i: Index) -> Element { get { storage[i.storageIndex] } set { storage[i.storageIndex] = newValue } } } // End expansion for "#makeArrayND" #makeArrayND(n: 3) #makeArrayND(n: 4) #makeArrayND(n: 5)
-
11:23 - The @AddCompletionHandler peer macro: motivation
/// Fetch the avatar for the user with `username`. func fetchAvatar(_ username: String) async -> Image? { ... } func fetchAvatar(_ username: String, onCompletion: @escaping (Image?) -> Void) { Task.detached { onCompletion(await fetchAvatar(username)) } }
-
11:51 - The @AddCompletionHandler peer macro: macro declaration
/// Overload an `async` function to add a variant that takes a completion handler closure as /// a parameter. @attached(peer, names: overloaded) macro AddCompletionHandler(parameterName: String = "completionHandler")
-
11:59 - The @AddCompletionHandler peer macro: usage
/// Fetch the avatar for the user with `username`. @AddCompletionHandler(parameterName: "onCompletion") func fetchAvatar(_ username: String) async -> Image? { ... } // Begin expansion for "@AddCompletionHandler" /// Fetch the avatar for the user with `username`. /// Equivalent to ``fetchAvatar(username:)`` with /// a completion handler. func fetchAvatar( _ username: String, onCompletion: @escaping (Image?) -> Void ) { Task.detached { onCompletion(await fetchAvatar(username)) } } // End expansion for "@AddCompletionHandler"
-
12:36 - The @DictionaryStorage accessor macro: motivation
struct Person: DictionaryRepresentable { init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] var name: String { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } var height: Measurement<UnitLength> { get { dictionary["height"]! as! Measurement<UnitLength> } set { dictionary["height"] = newValue } } var birthDate: Date? { get { dictionary["birth_date"] as! Date? } set { dictionary["birth_date"] = newValue as Any? } } }
-
13:04 - The @DictionaryStorage accessor macro: declaration
/// Adds accessors to get and set the value of the specified property in a dictionary /// property called `storage`. @attached(accessor) macro DictionaryStorage(key: String? = nil)
-
13:20 - The @DictionaryStorage accessor macro: usage
struct Person: DictionaryRepresentable { init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] @DictionaryStorage var name: String // Begin expansion for "@DictionaryStorage" { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } // End expansion for "@DictionaryStorage" @DictionaryStorage var height: Measurement<UnitLength> // Begin expansion for "@DictionaryStorage" { get { dictionary["height"]! as! Measurement<UnitLength> } set { dictionary["height"] = newValue } } // End expansion for "@DictionaryStorage" @DictionaryStorage(key: "birth_date") var birthDate: Date? // Begin expansion for "@DictionaryStorage" { get { dictionary["birth_date"] as! Date? } set { dictionary["birth_date"] = newValue as Any? } } // End expansion for "@DictionaryStorage" }
-
13:56 - The @DictionaryStorage member attribute macro: macro declaration
/// Adds accessors to get and set the value of the specified property in a dictionary /// property called `storage`. @attached(memberAttribute) @attached(accessor) macro DictionaryStorage(key: String? = nil)
-
14:46 - The @DictionaryStorage member attribute macro: usage
@DictionaryStorage struct Person: DictionaryRepresentable { init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] // Begin expansion for "@DictionaryStorage" @DictionaryStorage // End expansion for "@DictionaryStorage" var name: String // Begin expansion for "@DictionaryStorage" @DictionaryStorage // End expansion for "@DictionaryStorage" var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? }
-
15:52 - The @DictionaryStorage member macro: macro definition
/// Adds accessors to get and set the value of the specified property in a dictionary /// property called `storage`. @attached(member, names: named(dictionary), named(init(dictionary:))) @attached(memberAttribute) @attached(accessor) macro DictionaryStorage(key: String? = nil)
-
16:26 - The @DictionaryStorage member macro: usage
// The @DictionaryStorage member macro @DictionaryStorage struct Person: DictionaryRepresentable { // Begin expansion for "@DictionaryStorage" init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] // End expansion for "@DictionaryStorage" var name: String var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? }
-
16:59 - The @DictionaryStorage conformance macro: macro definition
/// Adds accessors to get and set the value of the specified property in a dictionary /// property called `storage`. @attached(conformance) @attached(member, names: named(dictionary), named(init(dictionary:))) @attached(memberAttribute) @attached(accessor) macro DictionaryStorage(key: String? = nil)
-
17:09 - The @DictionaryStorage conformance macro: usage
struct Person // Begin expansion for "@DictionaryStorage" : DictionaryRepresentable // End expansion for "@DictionaryStorage" { var name: String var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? }
-
17:28 - @DictionaryStorage starting point
struct Person: DictionaryRepresentable { init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] var name: String { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } var height: Measurement<UnitLength> { get { dictionary["height"]! as! Measurement<UnitLength> } set { dictionary["height"] = newValue } } var birthDate: Date? { get { dictionary["birth_date"] as! Date? } set { dictionary["birth_date"] = newValue as Any? } } }
-
17:32 - @DictionaryStorage ending point
@DictionaryStorage struct Person // Begin expansion for "@DictionaryStorage" : DictionaryRepresentable // End expansion for "@DictionaryStorage" { // Begin expansion for "@DictionaryStorage" init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] // End expansion for "@DictionaryStorage" // Begin expansion for "@DictionaryStorage" @DictionaryStorage // End expansion for "@DictionaryStorage" var name: String // Begin expansion for "@DictionaryStorage" { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } // End expansion for "@DictionaryStorage" // Begin expansion for "@DictionaryStorage" @DictionaryStorage // End expansion for "@DictionaryStorage" var height: Measurement<UnitLength> // Begin expansion for "@DictionaryStorage" { get { dictionary["height"]! as! Measurement<UnitLength> } set { dictionary["height"] = newValue } } // End expansion for "@DictionaryStorage" @DictionaryStorage(key: "birth_date") var birthDate: Date? // Begin expansion for "@DictionaryStorage" { get { dictionary["birth_date"] as! Date? } set { dictionary["birth_date"] = newValue as Any? } } // End expansion for "@DictionaryStorage" }
-
17:35 - @DictionaryStorage ending point (without expansions)
@DictionaryStorage struct Person { var name: String var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? }
-
18:01 - Macro implementations
/// Creates a tuple containing both the result of `expr` and its source code represented as a /// `String`. @freestanding(expression) macro stringify<T>(_ expr: T) -> (T, String) = #externalMacro( module: "MyLibMacros", type: "StringifyMacro" )
-
19:18 - Implementing @DictionaryStorage’s @attached(member) role (1)
import SwiftSyntax import SwiftSyntaxMacros import SwiftSyntaxBuilder struct DictionaryStorageMacro: MemberMacro { static func expansion( of attribute: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { return [ "init(dictionary: [String: Any]) { self.dictionary = dictionary }", "var dictionary: [String: Any]" ] } }
-
19:52 - Code used to demonstrate SwiftSyntax trees
@DictionaryStorage struct Person { var name: String var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? }
-
22:00 - Implementing @DictionaryStorage’s @attached(member) role (2)
import SwiftSyntax import SwiftSyntaxMacros import SwiftSyntaxBuilder struct DictionaryStorageMacro: MemberMacro { static func expansion( of attribute: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { return [ "init(dictionary: [String: Any]) { self.dictionary = dictionary }", "var dictionary: [String: Any]" ] } }
-
24:29 - A type that @DictionaryStorage isn’t compatible with
@DictionaryStorage enum Gender { case other(String) case female case male // Begin expansion for "@DictionaryStorage" init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] // End expansion for "@DictionaryStorage" }
-
25:17 - Expansion method with error checking
import SwiftSyntax import SwiftSyntaxMacros import SwiftSyntaxBuilder struct DictionaryStorageMacro: MemberMacro { static func expansion( of attribute: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { guard declaration.is(StructDeclSyntax.self) else { let structError = Diagnostic( node: attribute, message: MyLibDiagnostic.notAStruct ) context.diagnose(structError) return [] } return [ "init(dictionary: [String: Any]) { self.dictionary = dictionary }", "var dictionary: [String: Any]" ] } } enum MyLibDiagnostic: String, DiagnosticMessage { case notAStruct var severity: DiagnosticSeverity { return .error } var message: String { switch self { case .notAStruct: return "'@DictionaryStorage' can only be applied to a 'struct'" } } var diagnosticID: MessageID { MessageID(domain: "MyLibMacros", id: rawValue) } }
-
29:32 - Parameter list for `ArrayND.makeIndex`
FunctionParameterListSyntax { for dimension in 0 ..< numDimensions { FunctionParameterSyntax( firstName: .wildcardToken(), secondName: .identifier("i\(dimension)"), type: TypeSyntax("Int") ) } }
-
30:17 - The #unwrap expression macro: revisited
let image = #unwrap(downloadedImage, message: "was already checked") // Begin expansion for "#unwrap" { [downloadedImage] in guard let downloadedImage else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + "was already checked", file: "main/ImageLoader.swift", line: 42 ) } return downloadedImage }() // End expansion for "#unwrap"
-
30:38 - Implementing the #unwrap expression macro: start
static func makeGuardStmt() -> StmtSyntax { return """ guard let downloadedImage else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + "was already checked", file: "main/ImageLoader.swift", line: 42 ) } """ }
-
30:57 - Implementing the #unwrap expression macro: the message string
static func makeGuardStmt(message: ExprSyntax) -> StmtSyntax { return """ guard let downloadedImage else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + \(message), file: "main/ImageLoader.swift", line: 42 ) } """ }
-
31:21 - Implementing the #unwrap expression macro: the variable name
static func makeGuardStmt(wrapped: TokenSyntax, message: ExprSyntax) -> StmtSyntax { return """ guard let \(wrapped) else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + \(message), file: "main/ImageLoader.swift", line: 42 ) } """ }
-
31:44 - Implementing the #unwrap expression macro: interpolating a string as a literal
static func makeGuardStmt(wrapped: TokenSyntax, message: ExprSyntax) -> StmtSyntax { let messagePrefix = "Unexpectedly found nil: ‘downloadedImage’ " return """ guard let \(wrapped) else { preconditionFailure( \(literal: messagePrefix) + \(message), file: "main/ImageLoader.swift", line: 42 ) } """ }
-
32:11 - Implementing the #unwrap expression macro: adding an expression as a string
static func makeGuardStmt(wrapped: TokenSyntax, originalWrapped: ExprSyntax, message: ExprSyntax) -> StmtSyntax { let messagePrefix = "Unexpectedly found nil: ‘\(originalWrapped.description)’ " return """ guard let \(wrapped) else { preconditionFailure( \(literal: messagePrefix) + \(message), file: "main/ImageLoader.swift", line: 42 ) } """ }
-
33:00 - Implementing the #unwrap expression macro: inserting the file and line numbers
static func makeGuardStmt(wrapped: TokenSyntax, originalWrapped: ExprSyntax, message: ExprSyntax, in context: some MacroExpansionContext) -> StmtSyntax { let messagePrefix = "Unexpectedly found nil: ‘\(originalWrapped.description)’ " let originalLoc = context.location(of: originalWrapped)! return """ guard let \(wrapped) else { preconditionFailure( \(literal: messagePrefix) + \(message), file: \(originalLoc.file), line: \(originalLoc.line) ) } """ }
-
34:05 - The #unwrap expression macro, with a name conflict
let wrappedValue = "🎁" let image = #unwrap(request.downloadedImage, message: "was \(wrappedValue)") // Begin expansion for "#unwrap" { [wrappedValue = request.downloadedImage] in guard let wrappedValue else { preconditionFailure( "Unexpectedly found nil: ‘request.downloadedImage’ " + "was \(wrappedValue)", file: "main/ImageLoader.swift", line: 42 ) } return wrappedValue }() // End expansion for "#unwrap"
-
34:30 - The MacroExpansion.makeUniqueName() method
let captureVar = context.makeUniqueName() return """ { [\(captureVar) = \(originalWrapped)] in \(makeGuardStmt(wrapped: captureVar, …)) \(makeReturnStmt(wrapped: captureVar)) } """
-
35:44 - Declaring a macro’s names
@attached(conformance) @attached(member, names: named(dictionary), named(init(dictionary:))) @attached(memberAttribute) @attached(accessor) macro DictionaryStorage(key: String? = nil) @attached(peer, names: overloaded) macro AddCompletionHandler(parameterName: String = "completionHandler") @freestanding(declaration, names: arbitrary) macro makeArrayND(n: Int)
-
38:28 - Macros are testable
import MyLibMacros import XCTest import SwiftSyntaxMacrosTestSupport final class MyLibTests: XCTestCase { func testMacro() { assertMacroExpansion( """ @DictionaryStorage var name: String """, expandedSource: """ var name: String { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } """, macros: ["DictionaryStorage": DictionaryStorageMacro.self]) } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.
쿼리를 제출하는 중에 오류가 발생했습니다. 인터넷 연결을 확인하고 다시 시도해 주세요.