매크로, 그냥 쓰기만 했지 만들어본 적은 없죠?
Rust를 쓰다 보면 #[derive(Debug)]나 #[derive(Clone)] 같은 어트리뷰트를 정말 자주 만나요. 구조체 위에 한 줄 붙였을 뿐인데 출력 기능이나 복사 기능이 알아서 생겨나죠. 이게 바로 절차적 매크로(procedural macro)의 힘이에요. 그런데 막상 "이걸 내가 직접 만들 수 있나?" 하면 대부분 멈칫해요. 오늘은 비트필드(bitfield) 매크로를 예로 절차적 매크로의 세계를 풀어볼게요.
절차적 매크로가 뭐냐면
쉽게 말하면 컴파일 시점에 코드를 입력받아서 코드를 출력하는 함수예요. 일반 함수는 실행 중에 값을 받아 값을 돌려주지만, 절차적 매크로는 컴파일하는 동안 여러분이 작성한 소스코드 자체를 입력으로 받아요. 그걸 분석하고 변형해서 새로운 소스코드를 뱉어내면, 컴파일러가 그 결과를 마치 원래 작성된 코드인 것처럼 이어서 컴파일하는 거예요. 그래서 "코드를 짜는 코드", 즉 메타프로그래밍이라고 부르죠.
여기서 핵심 개념이 TokenStream이에요. 이게 뭐냐면, 소스코드를 잘게 쪼갠 토큰들의 흐름이에요. struct, Foo, {, } 같은 조각들이 순서대로 들어 있는 거죠. 절차적 매크로 함수는 TokenStream을 받아서 TokenStream을 반환해요. 입력도 코드 조각, 출력도 코드 조각인 셈이에요.
세 가지 종류와 핵심 도구
절차적 매크로는 크게 세 종류예요. 구조체에 붙여서 코드를 생성하는 derive 매크로(#[derive(...)]), 임의의 어트리뷰트를 만드는 attribute 매크로, 그리고 함수처럼 호출하는 function-like 매크로(my_macro!(...))죠. 비트필드는 보통 derive나 attribute 형태로 만들어요.
날것의 TokenStream을 직접 파싱하는 건 너무 고통스럽기 때문에, Rust 생태계에는 사실상 표준이 된 두 크레이트가 있어요. syn은 TokenStream을 구조체, 필드, 타입 같은 의미 있는 문법 트리(AST)로 파싱해줘요. "이 토큰 덩어리는 사실 필드 3개짜리 구조체야"라고 해석해주는 거죠. 반대로 quote는 우리가 만들고 싶은 코드를 거의 그대로 적으면 TokenStream으로 바꿔줘요. quote! { fn foo() {} }처럼 템플릿을 쓰듯 코드를 생성할 수 있어요.
비트필드 매크로는 무슨 일을 할까
비트필드가 뭐냐면, 하나의 정수 안에 여러 작은 값을 비트 단위로 욱여넣는 기법이에요. 예를 들어 8비트짜리 u8 하나에 "플래그 1비트 + 우선순위 3비트 + 타입 4비트"를 나눠 담는 거죠. 임베디드나 OS 커널, 네트워크 프로토콜 파싱에서 메모리를 아끼려고 자주 써요.
문제는 이걸 손으로 하면 (value >> 3) & 0b111 같은 비트 시프트와 마스크 연산을 일일이 써야 해서 실수하기 딱 좋다는 거예요. 비트필드 매크로는 이 귀찮은 일을 대신 해줘요. 여러분이 "이 필드는 몇 번째 비트부터 몇 비트"라고 선언만 하면, 매크로가 그에 맞는 게터(get)와 세터(set) 메서드를 자동으로 생성해줘요. syn으로 각 필드의 비트 폭을 읽어내고, quote로 시프트·마스크 코드를 짜서 끼워 넣는 거죠. 사용자는 flags.priority()처럼 깔끔한 메서드만 쓰면 되고요.
업계 맥락
Rust 진영에는 이미 bitfield, modular-bitfield, bitflags 같은 검증된 크레이트가 있어요. 그래서 실전에서는 직접 만들기보다 이런 라이브러리를 쓰는 게 보통이에요. 그럼에도 직접 만들어보는 게 가치 있는 이유는, 이 과정을 한 번 거치면 Rust 컴파일러가 코드를 어떻게 보는지가 손에 잡히기 때문이에요. C의 전처리기 매크로가 단순 텍스트 치환이라 위험했던 것과 달리, Rust의 절차적 매크로는 문법 구조를 제대로 이해한 상태에서 동작해서 훨씬 안전하고 강력해요. 이 차이를 체감하는 것만으로도 배움이 커요.
한국 개발자에게 주는 시사점
반복되는 보일러플레이트 코드 때문에 지친 적 있다면 절차적 매크로가 답이 될 수 있어요. DTO 변환, 직렬화, 검증 로직처럼 "패턴은 같은데 타입마다 반복해서 써야 하는" 코드를 매크로 하나로 자동 생성할 수 있거든요. 실제로 serde가 그렇게 동작하죠. 다만 매크로는 디버깅이 어렵고 컴파일 시간을 늘릴 수 있으니, cargo expand로 생성된 코드를 확인하는 습관을 같이 들이는 걸 추천해요.
마무리
절차적 매크로는 결국 syn으로 코드를 읽고, 변형하고, quote로 다시 코드를 쓰는 일이에요. 비트필드 같은 작은 예제로 시작하면 그 흐름이 의외로 명확하게 잡혀요.
여러분은 프로젝트에서 어떤 반복 코드를 매크로로 없애보고 싶나요? 매크로의 편리함과 디버깅의 어려움, 어느 쪽이 더 크다고 느끼시나요?
🔗 출처: Hacker News
TTJ 코딩클래스 정규반
월급 외 수입,
코딩으로 만들 수 있습니다
17가지 수익 모델을 직접 실습하고, 1,300만원 상당의 자동화 도구와 소스코드를 받아가세요.
"비전공 직장인인데 반년 만에 수익 파이프라인을 여러 개 만들었습니다"
실제 수강생 후기- 비전공자도 6개월이면 첫 수익
- 20년 경력 개발자 직강
- 자동화 프로그램 + 소스코드 제공