13. 매크로: 코드를 짜는 코드 🟡
학습 목표:
- 패턴 매칭과 반복을 이용한 **선언적 매크로(
macro_rules!)**를 작성합니다.- 매크로를 사용해야 할 때와 제네릭/트레이트로 충분할 때를 구분합니다.
- 절차적 매크로(Procedural Macros):
derive,attribute,function-like세 가지 유형을 이해합니다.syn과quote크레이트를 활용해 커스텀 Derive 매크로를 구현해 봅니다.
선언적 매크로 (macro_rules!)
선언적 매크로는 코드의 구문 패턴을 매칭하여 컴파일 타임에 코드를 확장합니다.
#![allow(unused)] fn main() { // 간단한 HashMap 생성 매크로 macro_rules! hashmap { ( $( $key:expr => $value:expr ),* $(,)? ) => { { let mut map = std::collections::HashMap::new(); $( map.insert($key, $value); )* map } }; } let scores = hashmap! { "Alice" => 95, "Bob" => 87, }; }
매크로 프래그먼트 (Fragment) 종류
매크로에서 매칭할 수 있는 코드 조각의 타입들입니다.
| 이름 | 매칭 대상 | 예시 |
|---|---|---|
$x:expr | 모든 표현식 | 42, a + b, foo() |
$x:ty | 타입 | i32, Vec<String> |
$x:ident | 식별자 (변수/함수명) | my_var, Config |
$x:path | 경로 | std::io::Error |
$x:tt | 단일 토큰 트리 | 가장 유연하며 무엇이든 매칭 가능 |
매크로 사용 시 주의사항
- 권장 상황: 트레이트/제네릭으로 해결하기 힘든 보일러플레이트 제거(테스트 케이스 자동 생성 등), DSL 구축(
html!,sql!). - 지양 상황: 단순한 함수나 제네릭으로 해결 가능한 경우. 매크로는 디버깅이 어렵고 자동 완성이 잘 작동하지 않습니다.
절차적 매크로 (Procedural Macros)
절차적 매크로는 토큰 스트림을 입력받아 Rust 함수처럼 조작하여 새로운 토큰 스트림을 반환합니다.
- Derive 매크로:
#[derive(MyTrait)]처럼 구조체나 열거형에 트레이트 구현을 자동으로 추가합니다. - Attribute 매크로:
#[route(GET, "/")]처럼 아이템 자체를 변형합니다. - Function-like 매크로:
sql!(SELECT * FROM ...)처럼 커스텀 구문을 정의합니다.
매크로 위생(Hygiene)과 $crate
매크로 내부에서 정의한 변수 이름이 매크로를 호출한 곳의 변수 이름과 충돌하지 않도록 보장하는 것을 위생이라고 합니다. 또한 라이브러리에서 매크로를 정의할 때는 사용자가 크레이트를 어떤 이름으로 임포트했든 상관없이 작동하도록 항상 $crate 키워드를 사용하여 자신을 참조해야 합니다.
📝 연습 문제: 선언적 매크로 map! 제작 ★ (~15분)
키-값 쌍을 인자로 받아 HashMap을 생성하는 map! 매크로를 작성해 보세요. 쉼표가 마지막에 있거나 아예 인자가 없는 상황(map!{})도 지원해야 합니다.
📌 요약
- 단순 반복 코드는 **
macro_rules!**로 해결하세요. - 복잡한 구조체 분석이나 코드 생성에는 **
syn**과 **quote**를 활용한 절차적 매크로가 적합합니다. - 매크로를 설계할 때는 항상 함수나 트레이트로 더 나은 해결책이 있는지 먼저 고민하세요.
- 라이브러리용 매크로에서는 호환성을 위해 반드시 **
$crate**를 사용하세요.