NameValue 속성에 대해서 파싱하는 방법을 알아보겠다.
struct Test {
#[default = "kim"]
name: String,
}
위처럼 default값을 설정하는 NameValue 속성을 파싱 하여 자동으로 코드를 생성해 보도록 하겠다.
TokenStream을 syn크레이트를 활용하여 DeriveInput으로 파싱할 수 있다.
panic!은 런타임에 프로그램이 비정상적으로 종료될 때 사용된다. 하지만 매크로는 컴파일 타임에 코드를 생성하기 때문에, 매크로 내에서 문제가 발생했다면 컴파일 과정에서 해당 문제를 포착하여 친절하고 명확한 에러 메시지를 사용자에게 보여주는 것이 더 좋다.
compile_error!는 매크로 확장이 실패하면 컴파일러가 사용자에게 에러 메시지를 직접 전달하여, 사용자가 코드를 수정할 수 있도록 도와준다.
#[proc_macro_derive(Test, attributes(a,b,c, default))]
pub fn test_derive_macro(input: TokenStream) -> TokenStream {
let ast: DeriveInput = syn::parse(input).unwrap();
// 일단 필드가 하나라고 가정..
let Data::Struct(struct_data) = ast.data else {
return quote! {
compile_error!("구조체에만 사용하세요.");
}.into();
};
quote! {
}.into()
}
테스트 하기위해서 간단하게 매크로를 만들었다. Enum에 사용해보도록 하겠다.
#[derive(Test)]
enum FailEnum {
#[default = "kim"]
Name(String),
}

처음으로 compile_error!를 사용해 봤는데 뭐가 잘못되었는지 확실하게 알 수 있는 것 같다. 매크로의 경우 은근 개발 중간에 어디가 문제인지 찾기 힘든데 좋은 꿀팁인 것 같다.
#[proc_macro_derive(Test, attributes(a,b,c, default))]
pub fn test_derive_macro(input: TokenStream) -> TokenStream {
let ast: DeriveInput = syn::parse(input).unwrap();
let ident = ast.ident;
// 일단 필드가 하나라고 가정..
let Data::Struct(struct_data) = ast.data else {
return quote! {
compile_error!("구조체에만 사용하세요.");
}.into();
};
let default_field_map = struct_data.fields.iter().map(|f| {
let Some(value) = parsing_default_attribute(f) else {
return quote! {}.into();
};
let ident = f.ident.clone().unwrap();
quote! {
#ident: #value
}
});
quote! {
impl #ident {
pub fn new() -> Self {
Self {
#(#default_field_map,)*
}
}
}
}.into()
}
fn parsing_default_attribute(field: &Field) -> Option<LitStr> {
let Some(res) = field.attrs.iter().find_map(|attr| {
// attribute의 이름이 `default`인지 확인
// default가 아니라면 return
if !attr.meta.path().is_ident("default") {
// default에 매칭되는 value를 구해야함
return None;
}
// attribute가 NameValue가 아니라면 return
let Meta::NameValue(meta_name_value) = &attr.meta else {
return None;
};
// 일단 여기서는 String인 경우만 취급하겠음...
let Expr::Lit(lit) = &meta_name_value.value else {
return None;
};
let Lit::Str(val) = &lit.lit else {
return None;
};
Some(val.clone())
}) else {
return None;
};
Some(res)
}
위처럼 매크로를 생성을 했다. 더 디테일하게 에러 핸들링 및 처리를 해줘야하지만 일단 스터디용으로 작성한 것이므로 넘어가겠다.
#[derive(Test)]
struct DefaultTest {
#[default = "kim"]
name: String,
}
위처럼 derive에 넣어주었는데

이런 오류가 발생한다.
이 오류 메시지의 문제점은 LitStr이 "kim"으로 파싱 하기 때문에 into() 또는 .to_string()을 붙여서 TokenStream을 만들어 줘야 한다.
quote! {
#ident: #value.into()
}
fn main() {
let res = DefaultTest::new();
println!("name: {}", res.name);
}

결국 proc_macro는 코드를 구조화된 트리 형태로 파싱 하여 사용자가 원하는 코드를 컴파일 타임에 만들어주는 것 같다.
여러 번 매크로를 생성하여 사용해 봤지만 이번 기회를 통해 정확히 알아보았다.(그래도 잼미니에게 물어봐야함)
위에 작성한 코드의 경우 좀 더 다듬어야 한다. 추가로 여러 속성들이 생길 경우 복잡해질 것이다.
다음에는 좀 더 복잡한 매크로를 생성해 볼 예정이다.