SWIFT

[Swift] Macro (1)

윤동민 2024. 9. 16. 16:35
반응형

이전에 Macros라는 Swift 언어의 기능이 나왔다고 알고있었는데, 느낌만 알고있다 이번에 SwiftUI + TCA 스터디를 진행하다보니 TCA에서 Macros를 활용하는 경우가 보여서 어떤 기능인지 자세히 알아보려고합니다 🙂

 

만든 이유

우선 Macros의 개발자문서 설명을 보면 '컴파일 타임에 코드를 발생시킨다'고 되어있는데요. 뭐 여기 본문만 보면은 어떤 느낌인지 감이 잘안오는데, 한번 알아보면서 살살 잡아가보자구요

Use macros to generate code at compile time

좀더 자세한 설명을 보면 개발자들이 코드의 반복을 피하기위해 우리의 코드를 컴파일 타임에 전환해준다고하는데요. 그림을 보면 우리가 작성한 코드가 'Expanded code source'로 확장되는 모습을 볼수있습니다.

 

아마도 우리가 메소드를 재사용하는것처럼 반복되는 코드들을 피하기위해 Swift에서 제공해주는 새로운 기능인 것 같습니다

Macros의 기능을 이용하면 컴파일러를 수정하지 않고도 Swift Package에 개발자가 원하는 기능을 배포할 수 있게 해주는데 오픈소스인 Swift에 기능을 추가하는 방법보다 좀 더 간편하게 개발자가 기능을 추가할 수 있게 지원하기위해 도입된 기능인 것 같습니다 😃

 

디자인 목표

  1. 기능을 사용할 때, Macro를 사용하고 있다는게 명확해야합니다. Macro에는 크게 2가지 Freestanding & Attached 두가지가 존재합니다. 각각 아래의 특징을 가지고 있기 때문에, Macro를 사용하고있구나 알수있습니다.
    1. Freestanding은 항상 #을 prefix로 붙이고 있다.
    2. Attached는 항상 @을 prefix로 붙이고 있다
  2. Macro에 넘긴 코드와 Macro에서 넘어온 코드 둘다 완전하고 실패를 확인할 수 있어야한다.
    1. 뭔가 잘못된 인자를 넘기거나 잘못된 인자가 넘어오는 경우 컴파일러 에러로 이를 올바르게 쓰고 있는지 확인이 가능해야한다.
  3. Macro 전개가 예상가능한 방법으로 프로그램에 포함되어야한다.
  4. Macro는 마법이 되면안된다. 어디로 전개되거나 어떻게 실행되는지 알수있어야하고 이를 실행되는 곳에서 디버깅이 가능하고 펼쳐볼수도 있습니다.

 

이제부터는 각각 어떤 매크로의 기능들이 있고 어떤 역할들을 수행하는지 알아보겠습니다

 

Macro Role

이제부터는 각각 어떤 매크로의 기능들이 있고 어떤 역할들을 수행하는지 알아보겠습니다.

우선 Macro는 크게 2가지(Freestanding & Attached)가 존재하는데 각각에 어떤 속성들이 있고 어떤 역할을 하는지 알아보겠습니다 :)

 

@freestanding(expression) : 실행해서 어떤 결과를 생성하는 것에 대한것을 담당


그래서 그게 뭔데,,,, 어떻게 사용하는건데 아래 예시와 함께 살펴보겠습니다

이런 상황에서 이런 것을 Macro로 선언해서 만들어주면 좀 더 균형잡힌 코드로 표현하는 것이 가능합니다.

물론 위의 코드처럼 이렇게 사용하는데 내부에는 unwrap Macro를 구현하고 있어야합니다. 물론 사용하는 부분에서 실제 Macro가 어떻게 구현되어있는지 이처럼 볼수도 있습니다.

 

근데 만약 guard let을 이용해서 동일하게 unwrapping을 해야하는 코드가 많다면 직접 코드를 작성하는게 번거로울 수 있겠죠..?

이때 이렇게 @freestanding(expression)을 이용하면 좀 더 보일러플레이트 코드들을 줄이는 방향으로 사용이 가능합니다.

 

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

 

이건 또 무슨 소리야…..? 또 예시를 통해 알아보겠습니다

 

예로 2차원 배열이 있는데 인덱스를 1차원 배열의 인덱스로 변경해서 산출하길 원하는 경우가 있다고라고 생각해보면 아래와 같은 코드가 나와야합니다.

이게 코드를 이해하기보다는 (1,0) → 4, (0,1) → 1 요런식으로 아래의 ‘2차원 배열의 인덱스가 1차원 배열의 인덱스로 바뀐다’라고만 이해하고보면 될 것 같습니다

근데 만약 여기서 3차원 배열에서도 동일한 구현이 필요하다고하면 또 한번 매우 비슷한 선언이 아래와 같이 만들어지게 됩니다. 

그리고 여기서 만약 4, 5, 6…N차까지 필요하게되면 거의 비슷한 구현들이 계속 선언되게 되겠죠.

바로 이때, 활용할 수 있는것이 @freestanding(declaration)입니다.

이렇게 선언하고 사용할 수 있는데, 이때보면 @freestanding(expression)과는 다르게 return 타입을 지정하지 않습니다. 바로 이 Macro는 선언을 추가하는 역할을 하기 때문에 필요하지 않습니다.

 

@attached(peer) : 어떤 선언에든 첨부되어서 변수, 함수, 타입, 연산자 선언까지 아우르며 옆에 새로운 선언을 삽입

 

이번에도 그냥 보고 무슨 역할을 하는지 알아보기는 힘드니 예시로 보겠습니다 !

 

여기서는 만약 async로 작성한 메소드가 있는데, 이를 예전 버전을 지원하기 위해 completion을 넘기는 API가 필요한 경우 아래와 같이 새롭게 작성하는건 큰 문제가 없습니다.

하지만 이런 경우가 많아서 이에 대해 새롭게 API를 작성하다보면 이 과정이 번거롭게 느껴집니다. 이때 활용할 수 있는것이 @attached(peer)입니다.

이렇게 선언한 것을 기존의 메소드에 선언해준다면,,,?!

바로 completionHandler 기반의 원본과 동일한 코드를 생성하고 확장되고 추가로 문서 주석까지 달아주게됩니다 🙂

 

@attached(accessor) : 변수에 추가될수있고 예로 get, set, willSet, didSet 이런것들을 삽입

 

예로 Person이라는 구조체에 Dictionary가 있고 이에 대해 각 Key 값으로 접근하는 name, height, birthDate들을 정의했습니다.

 

근데 이렇게 매번 새로운 Key 값에 대해 새롭게 getter, setter를 정의하는게 조금 번거롭습니다,,,

여기서 Property Wrapper를 사용하면 어떨까하지만 Property Wrapper는 각각의 변수에 대해 다른 Stored Property에 접근하기 때문에 여기서는 부적절합니다,,,

 

이때, @attached(accessor)를 사용할 수 있습니다.

이렇게 선언된 Macro를 사용하기위해 각 변수앞에 @DictionaryStorage를 붙여주면됩니다.

이렇게 붙여주면 Macro가 아래처럼 accessor를 생성해서 사용할수 있게 됩니다

 

근데, 여기서도 아직 많진 않지만 보일러 플레이트 코드가 존재합니다. 바로 @DictionaryStorage를 반복적으로 작성 해주어야 한다는 점입니다.

 

이런 점을 확장하기위해서 아래의 @attached(memberAttribute)를 사용할 수 있습니다

 

@attached(memberAttribute) : Type / Extension의 선언에 속성을 삽입

 

이번에는 새로운 Macro를 생성하는 대신에 앞에서 보았던 선언에 추가로 @attached(memberAttribute) 속성을 추가해줍니다.

하나의 매크로가 여러가지 속성을 가질수 있는거야..?

Macro에는 2가지 속성이 있다고 했는데 freestanding은 조합이 불가능하고 attached는 조합이 가능합니다. attached 속성을 여러가지를 부여하면 Swift에서 부여한 속성을 맥락에 맞게 확장시켜서 컴파일합니다.

프로퍼티에 첨부하면 @attached(accessor) 역할로 확장 타입에 첨부하면 @attached(memberAttribute) 역할로 확장 이를 메소드에 첨부하면 컴파일러 에러 발생 - DictionaryStorage는 메소드에 첨부할 수 있는 역할이 X

이를 이전에 모든 프로퍼티에 일일이 적용했던 것과 다르게 유형의 앞에 선언해주게되면 자동으로 전체 유형에 첨부가 가능합니다

실제로는 이렇게 추가가 되게 됩니다 그리고 실제 구현체에는 특정 멤버(initializer, 이미 속성을 가진 타입등)를 건너뛸수 있는 로직이 있을 것입니다.

 

@attached(member) : Type / Extension 내에 새로운 선언(메소드, 프로퍼티, 이니셜라이저)을 삽입

 

아직도 이전 예시에서는 보일러 플레이트 코드들이 남아있습니다. 바로 이니셜라이저와 저장프로퍼티입니다.

이들은 DictionaryRepresentable 프로토콜에서 요구하는 구현체입니다

이때 @attached(member) 속성을 사용할 수 있습니다.

이제 이렇게 추가해주게되면 해당 DictionaryStorage를 이용하는 것만으로 이니셜라이저와 저장 프로퍼티를 자동으로 추가해주게 됩니다.

 

물론 실제 Macro의 구현부에는 이니셜라이저와 저장 프로퍼티를 추가해주는 코드가 작성되어 있을겁니다 !

 

@attached(conformance) : Type / Extension에 프로토콜 conformance를 채택

 

아직도 여전이 보일러 플레이트 코드가 남아있는데요. 바로 DictionaryRepresentable 프로토콜 채택입니다.

이때는 @attached(conformance) 속성을 사용할 수 있습니다.

이렇게 추가하고 구현해주는 것만으로 이제 수작업으로 DictionaryRepresentable을 채택하는 부분을 작성하지 않아도 됩니다.

 

이것도 물론 구현부에는 해당 프로토콜을 채택하는 부분이 구현되어 있겠죠 ?

 

우선 오늘은 Macro가 왜 나왔고 어떤 디자인 목표를 가지고 있고 어떤역할을하고 기능들이 있는지만 가볍게 먼저 알아보았습니다 :)

 

다음 포스팅에서는 이를 어떻게 구현하는지 좀 더 자세한 부분들을 알아보도록 하겠습니다

감사합니다 😊

 


레퍼런스

 

Expand on Swift macros - WWDC23 - Videos - Apple Developer

Discover how Swift macros can help you reduce boilerplate in your codebase and adopt complex features more easily. Learn how macros can...

developer.apple.com

 

반응형