콘텐츠로 이동

Rust 매크로

문법 페이지에서 간단한 규칙을 하나 배웠죠. !가 붙어 있으면 일반 함수가 아니라 매크로(macro)라는 거예요. println!, vec!, format!처럼요. 이 페이지에서는 드디어 그게 실제로 무슨 뜻인지 설명해요.

println!이 일반 함수일 수 없는 이유

섹션 제목: “println!이 일반 함수일 수 없는 이유”

fn add(a: i32, b: i32) -> i32 같은 일반 함수는 언제나 정해진 타입의 인자를 정해진 개수만큼 받아요. 그런데 println!은 인자의 개수와 타입이 제각각이어도 호출할 수 있어요.

fn main() {
println!("인자가 없어요");
println!("{}", 42);
println!("{} 그리고 {}", "둘", 2);
}

실행 결과:

인자가 없어요
42
둘 그리고 2
▶ 직접 실행해 보기

이 호출들을 모두 받아들일 수 있는 평범한 함수 시그니처는 없어요. 매크로는 다르게 동작해요. 코드가 완전히 컴파일되기 전에 실행되어, 호출된 모양에 맞춰 실제 Rust 코드를 만들어 내요. println!("{} 그리고 {}", "둘", 2)는 사실상 정확히 그 두 인자를 처리하는 실제 코드로 펼쳐져요(expand). 호출이 다르면 펼쳐지는 코드도 달라요.

macro_rules!로 나만의 매크로를 정의할 수 있어요. 숫자를 제곱하는 매크로를 만들어 볼게요.

macro_rules! square {
($x:expr) => {
$x * $x
};
}
fn main() {
println!("{}", square!(5));
}

실행 결과:

25
▶ 직접 실행해 보기

($x:expr)는 “표현식 하나를 받아서 $x라고 부르자”라는 뜻이에요. => { ... } 부분은 매크로가 펼쳐질 코드예요. square!(5)라고 쓴 곳마다, 컴파일러는 실제로 무언가를 컴파일하기 전에 5 * 5로 바꿔 넣어요.

매크로는 더 복잡한 패턴도 매칭할 수 있어요. 반복되는 인자 목록도요. 이건 일반 함수로는 도저히 할 수 없는 일이에요.

macro_rules! sum {
($($x:expr),*) => {
{
let mut total = 0;
$(total += $x;)*
total
}
};
}
fn main() {
println!("{}", sum!(1, 2, 3));
println!("{}", sum!(10, 20));
}

실행 결과:

6
30
▶ 직접 실행해 보기

$($x:expr),*는 “쉼표로 구분된 0개 이상의 표현식”이라는 뜻이에요. 매크로 정의 하나로 vec![1, 2, 3]과 vec![1, 2, 3, 4, 5]가 모두 동작하는 것도 바로 이 패턴 덕분이에요.

매크로에는 세미콜론으로 구분된 규칙을 여러 개 둘 수 있어요. Rust는 규칙을 위에서부터 아래로 시도해서, 처음으로 맞는 규칙을 써요. 규칙 안에서 매크로를 다시 호출할 수도 있어요. 인자 개수에 상관없이 동작하는 max!를 볼게요.

macro_rules! max {
// 규칙 1: 값이 하나뿐이에요. 가장 큰 값은 그 값 자신이에요.
($x:expr) => {
$x
};
// 규칙 2: 값 하나, 그 뒤에 하나 이상의 다른 값들이 와요.
($x:expr, $($rest:expr),+) => {
{
let first = $x;
let biggest_of_rest = max!($($rest),+);
if first > biggest_of_rest { first } else { biggest_of_rest }
}
};
}
fn main() {
println!("{}", max!(7));
println!("{}", max!(3, 9, 4));
println!("{}", max!(2.5, 1.0));
}

실행 결과:

7
9
2.5
▶ 직접 실행해 보기

max!(3, 9, 4)에서는 값이 여러 개라 규칙 1이 맞지 않으니, 규칙 2가 맞아요. 규칙 2는 3을 옆에 두고 max!(9, 4)를 펼치고, 그건 다시 max!(4)를 펼쳐요. max!(4)는 규칙 1에 맞아서 거기서 멈춰요. 순서가 중요해요. Rust는 언제나 규칙 1부터 시도해요.

여기서 새로운 문법이 두 가지 나왔어요.

  • +는 하나 이상, *(sum!에서 썼죠)는 0개 이상이라는 뜻이에요. 규칙 2는 쉼표 뒤에 값이 적어도 하나 있어야 해서, 안쪽의 max!(...) 호출에는 언제나 처리할 값이 있어요.
  • max!는 정수에도 실수에도 동작해요. 매크로는 first > biggest_of_rest라는 코드를 그냥 붙여 넣을 뿐이니까요. 함수로 같은 일을 하려면 제네릭이 필요해요.

매크로가 받을 수 있는 건 expr만이 아니에요. 실제 매크로에서 보게 될 몇 가지를 소개할게요.

조각(fragment) 매칭되는 것 예
expr 모든 표현식 2 + 3, name.len()
ident 이름(식별자) count, Point
ty 타입 i32, Vec<String>
literal 리터럴 값 42, "안녕"

매크로를 실제로 만나게 되는 곳

섹션 제목: “매크로를 실제로 만나게 되는 곳”

매크로는 늘 쓰게 될 거예요(println!, vec!, format!, #[derive(Debug)]). 하지만 직접 만들 일은 거의 없어요. 직접 매크로를 만드는 건 주로 라이브러리 작성자들이 큰 코드베이스에서 정말로 반복되는 상용구(boilerplate) 코드를 없애려고 쓰는 틈새 기술이에요. 이런 게 있고 대략 어떻게 동작하는지 알아 두면 충분해요. 처음부터 꺼내 들 도구는 아니에요.

쉼표로 구분된 값을 몇 개든 받아서, 각 값을 한 줄씩 출력하는 매크로 print_all!을 만드세요. print_all!("사과", 42, 3.5)는 세 줄을 출력해야 해요. 힌트: sum!처럼 $($x:expr),*를 매칭하고, $( ... )*로 값마다 println!을 반복하세요.

정답 보기
macro_rules! print_all {
($($x:expr),*) => {
$(println!("{}", $x);)*
};
}
fn main() {
print_all!("사과", 42, 3.5);
}

실행 결과:

사과
42
3.5
▶ 직접 실행해 보기

이 매크로는 인자마다 하나씩, 세 개의 println! 호출로 펼쳐져요. 값마다 타입이 달라도 괜찮아요. 일반 함수로는 처리할 수 없는 일이에요.

vec!처럼 동작하는 my_vec!을 만드세요. my_vec![]은 빈 Vec을, my_vec![1, 2, 3]은 그 항목들이 든 Vec을 만들어요. 힌트: 규칙을 두 개 쓰세요. 첫 번째는 아무것도 없는 ()에 맞아요. 두 번째는 $($x:expr),+에 맞고, Vec::new()를 만든 다음 각 $x를 push해요.

정답 보기
macro_rules! my_vec {
() => {
Vec::new()
};
($($x:expr),+) => {
{
let mut v = Vec::new();
$(v.push($x);)+
v
}
};
}
fn main() {
let empty: Vec<i32> = my_vec![];
let numbers = my_vec![1, 2, 3];
println!("{:?} {:?}", empty, numbers);
}

실행 결과:

[] [1, 2, 3]
▶ 직접 실행해 보기

어떤 매크로든 (), [], {} 중 아무거나로 호출할 수 있어요. vec!처럼 목록 같은 매크로에는 보통 대괄호를 쓰지만, my_vec!(1, 2, 3)도 동작해요. 빈 Vec에는 : Vec<i32> 타입을 적어야 해요. 항목이 하나도 없으면 무엇을 담을지 Rust가 알 수 없거든요.

  • 매크로는 컴파일 전에 실제 코드로 펼쳐져요. 그래서 println!은 호출할 때마다 인자의 개수와 타입이 달라도 괜찮아요.
  • 이름 뒤에 붙은 !는 언제나 함수가 아니라 매크로를 호출한다는 표시예요.
  • macro_rules!로 나만의 매크로를 정의할 수 있어요. 표현식 하나($x:expr)나 반복되는 목록($($x:expr),*) 같은 패턴을 매칭해요.
  • 매크로에는 규칙을 여러 개 둘 수 있어요. Rust는 위에서부터 아래로 시도하고, 규칙 안에서 매크로를 다시 호출할 수도 있어요.
  • 매크로 작성은 틈새 기술이에요. 표준 라이브러리에 이미 있는 매크로를 쓰는 것만으로도 필요한 건 거의 다 해결돼요.

이해도 점검

1. println!을 일반 함수로 만들 수 없는 이유는 무엇일까요?

2. macro_rules! 패턴 안의 $x:expr는 무슨 뜻일까요?

3. 매크로는 언제 실제 코드로 펼쳐질까요?

점수: 0 / 3