Community라이팅 & 에디팅github.com

Maud (Rust HTML 매크로)

Rust의 compile-time HTML 매크로로, HTML 마크업을 Rust 코드 안에 인라인으로 작성하고 컴파일 시점에 타입 검사합니다.

Maud (Rust HTML 매크로)란 무엇인가요?

이 스킬은 Rust의 compile-time HTML 템플릿 엔진인 Maud(버전 0.27.x)에 대한 상세한 가이드를 제공합니다. Maud는 `html!` 매크로를 사용하여 HTML을 Rust 코드 내에 직접 작성하며, 컴파일 시점에 타입 안전성을 보장합니다. 이 스킬은 Maud의 설정 방법, 렌더링 API, Actix-web/Axum/Rocket/Warp 등 다양한 웹 프레임워크와의 통합, 요소 및 속성 작성법, 제어 구조(@if, @for, @match, @let), 이스케이프 처리, 사용자 정의 렌더링(Render 트레이트) 등을 포함합니다. 또한 컴포넌트 기반 템플릿 작성, `PreEscaped`를 통한 원시 HTML 삽입, `[condition]`을 활용한 조건부 속성/클래스 토글, `attr=[Option<T>]`를 통한 선택적 속성 값 설정 등 고급 기능도 다룹니다. 이 스킬은 Rust로 웹 애플리케이션을 개발하는 모든 개발자에게 유용하며, 특히 타입 안전한 HTML 템플릿을 원하는 경우에 적합합니다.

지원 대상~Claude Code~Codex CLI~Cursor✓Antigravity
npx skills add uwuclxdy/agenticat

Installed? Explore more 라이팅 & 에디팅 skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Maud (Rust HTML 매크로)은(는) 무엇을 하나요?

캡처된 날짜: 2026-07-10 (maud 0.27.0). 업데이트하려면: maud.lambda.xyz + docs.rs/maud에서 재확인한 후 변경 사항을 diff하세요.

Maud는 compile-time HTML 매크로(html!)입니다. 마크업이 Rust 코드 안에 인라인으로 존재하며, 컴파일 시점에 타입 검사를 수행합니다. 이 스킬은 maud 0.27.x (0.27.0, 2025-02-02 릴리스)를 대상으로 합니다.

Maud는 안정 Rust와 nightly에서 실행됩니다(nightly에서 더 나은 오류 메시지를 제공). 0.22.1(2020-11-02)부터 안정 Rust에서 사용 가능하며, 그 이전에는 nightly 전용 컴파일러 기능이 필요했습니다. MSRV는 문서화되어 있지 않습니다.


1. 설정

Cargo.toml

[dependencies]
maud = "0.27"

2. 렌더링 API

html! { ... }는 Markup 값을 반환합니다. Markup은 PreEscaped<String>의 타입 별칭으로, 이미 이스케이프된 HTML 문자열을 감싸는 래퍼입니다.

연산결과비고
html! { ... }Markup매크로 표현식의 값입니다.
markup.into_string()StringMarkup을 소비하여 빌드된 문자열을 반환합니다.
html! 내부의 (DOCTYPE)<!DOCTYPE html> 출력maud::DOCTYPE은 리터럴 문자열 <!DOCTYPE html>과 같은 상수입니다.
use maud::{html, DOCTYPE};

let page = html! {
    (DOCTYPE)
    html {
        head { title { "My site" } }
        body { p { "Hello" } }
    }
};

Display 값 렌더링: maud::display(x)는 템플릿 내부에서 Display 구현체를 통해 모든 값을 렌더링하는 자유 함수입니다. 이는 0.24.0에서 제거된 이전의 모든 Render for Display 구현을 대체합니다.

use maud::{html, display};

let n = 42;
html! { p { (display(n)) } };

참고: Markup/PreEscaped에는 Display 구현이 없습니다(docs.rs 0.27에서 확인, Render는 구현하지만 Display는 구현하지 않음). 원시 문자열이 필요하면 .into_string()으로 String으로 변환하세요. html_to! / 버퍼에 쓰는 매크로는 없습니다(html_debug!는 0.25.0에서 제거됨). 기존 버퍼에 쓰려면 Render 트레이트의 render_to를 사용하세요(§9 참조).


3. 웹 프레임워크 통합

아래 표에서 일치하는 기능 플래그를 활성화하면 Markup이 해당 트레이트를 구현합니다. 그러면 핸들러가 Markup을 직접 반환할 수 있습니다.

프레임워크기능 플래그Markup이 구현하는 트레이트종속성 고정 버전
기본값——아무것도 활성화되지 않음
Actix-webactix-webactix_web::Responderactix-web-dep + futures-util
AxumaxumIntoResponseaxum-core ^0.5 + http ^1
RocketrocketRocket의 Responderrocket ^0.5
Warpwarpwarp::Replywarp ^0.3.6
Poempoempoem::IntoResponsepoem ^3
Tidetidetide::Response에 대한 From<PreEscaped<String>>tide ^0.16.0
Submillisecondsubmillisecond (0.27.0의 새로운 기능)IntoResponsesubmillisecond ^0.4.1
Rouille수동, 아래 참조없음—

종속성 버전 고정은 docs.rs 기능 그래프에서 가져온 단일 소스입니다. 정확한 범위가 중요하다면 Cargo.toml을 확인하세요.

maud = { version = "0.27", features = ["axum"] }

핸들러 예제

Rocket:

#[get("/<name>")]
fn hello(name: &str) -> Markup {
    html! { h1 { "Hello, " (name) "!" } }
}

Actix-web과 Tide 핸들러는 반환 값을 Result로 감쌉니다: Actix async fn index() -> AwResult<Markup> { Ok(html! { ... }) }; Tide는 tide::Result<impl Into<Response>>로 추론되는 클로저에서 Ok(html! { ... })를 반환합니다(기능이 IntoResponse가 아닌 From<PreEscaped<String>>을 추가하므로, -> Markup 핸들러는 컴파일되지 않습니다). Axum, Poem, Submillisecond는 동기 또는 비동기 핸들러에서 Markup을 직접 반환합니다. 트레이트 구현이 각 프레임워크가 기대하는 응답 유형으로 변환합니다.

수동 경로 (기능 플래그 없음)

.into_string()을 호출하고 직접 문자열을 래핑합니다.

// Rouille에는 트레이트 구현이 없음:
Response::html(html! { h1 { "Hello, " (name) "!" } })

4. 요소와 중첩

내용이 있는 요소는 중괄호를 사용하며, 텍스트와 중첩된 요소는 내부에 들어갑니다.

빈 요소는 ;로 끝나며 본문을 갖지 않습니다. Maud는 XHTML(<br />)이 아닌 HTML5 구문(<br>)을 출력합니다.

html! {
    link rel="stylesheet" href="poetry.css";
    p {
        "Rock, you are a rock."
        br;
        "Gray, you are gray,"
    }
}

하이픈이 있는 요소와 속성이 지원됩니다(사용자 정의 요소, data-*, ARIA).

"컴파일 시점에 타입 검사됨"은 Rust 스플라이스 표현식 유형만 의미합니다. Maud는 HTML5 사양에 대해 요소 또는 속성 이름을 검증하지 않습니다. 콘텐츠 모델 중첩도 확인하지 않습니다(<p> 내부의 <div>는 문제없이 컴파일됨). 유효한 Rust 식별자는 오타를 포함하여 태그로 컴파일됩니다.

텍스트

html! 내부의 일반 큰따옴표 Rust 문자열 리터럴은 단순한 표현식입니다. 원시 문자열 리터럴은 길거나 특수 문자가 포함된 텍스트를 처리합니다.

html! {
    pre {
        r#"
            Rocks, these are my rocks.
            Smooth and round,
            Asleep in the ground.
        "#
    }
}

5. 속성

값 속성: name="value".

html! {
    a href="about:blank" { "Apple Bloom" }
    li class="lower-middle" { "Sweetie Belle" }
}

불리언/빈 속성: = 없이 이름만 사용합니다.

html! {
    input type="checkbox" name="cupcakes" checked;
    label for="cupcakes" { "Do you like cupcakes?" }
}

#id / .class 단축 표기

html! {
    input #cannon .big.scary.bright-red type="button" value="Launch";
    div."col-sm-2" { "Bootstrap column!" }
}
  • 여러 클래스 체이닝: .big.scary.bright-red.
  • 따옴표로 묶인 이름은 bare 식별자로 유효하지 않은 문자를 허용합니다: ."col-sm-2".
  • Rust 2021 공백 주의: # 앞에 공백이 있어야 합니다(input #pinkie;). 이전 에디션에서는 input#pinkie;가 허용되었습니다.

암시적 div

#id / .class 단축 표기가 있을 때 태그 이름을 생략하면 기본적으로 요소가 div가 됩니다.

html! {
    #main {
        "Main content!"
        .tip { "Storing food in a refrigerator can make it 20% cooler." }
    }
}

토글 [condition]

대괄호로 묶인 불리언 표현식은 불리언 속성 또는 클래스를 켜거나 끕니다.

let allow_editing = true;
html! {
    p contenteditable[allow_editing] { "Edit me." }
}

let cuteness = 95;
html! {
    p.cute[cuteness > 50] { "Squee!" }
}

선택적 속성 값 attr=[Option<T>]

=와 함께 대괄호로 묶인 Option은 Some일 때만 속성 값을 설정합니다. None은 속성을 완전히 생략합니다. 이는 존재 여부를 토글하는 bare [cond]와 다릅니다.

html! {
    p title=[Some("Good password")] { "Correct horse" }

    @let value = Some(42);
    input value=[value];

    @let title: Option<&str> = None;
    p title=[title] { "Battery staple" }   // title 생략됨
}

동적 이름

계산된 id를 #(...)로 스플라이스합니다. 리터럴과 스플라이스를 .{ } 내부에서 결합하여 클래스를 만듭니다.

let name = "rarity";
let severity = "critical";
html! {
    aside #(name) {
        p.{ "color-" (severity) } { "This is the worst! Possible! Thing!" }
    }
}

6. 스플라이스와 보간

(expr)는 런타임 값을 마크업에 삽입합니다. 값의 HTML 특수 문자는 기본적으로 이스케이프됩니다.

let best_pony = "Pinkie Pie";
let numbers = [1, 2, 3, 4];
html! {
    p { "Hi, " (best_pony) "!" }
    p {
        "I have " (numbers.len()) " numbers, "
        "and the first one is " (numbers[0])
    }
}

maud::Render 구현체가 있는 모든 유형을 스플라이스할 수 있습니다. 대부분의 기본 유형(str, i32, ...)은 이미 구현되어 있습니다.

임의의 Rust 로직을 위한 블록 표현식 스플라이스:

html! {
    p {
        ({
            let f: Foo = something_convertible_to_foo()?;
            f.time().format("%H%Mh")
        })
    }
}

속성의 스플라이스

=를 사용한 단일 값:

let secret_message = "Surprise!";
html! {
    p title=(secret_message) { "Nothing to see here." }
}

리터럴과 스플라이스를 연결하려면 { } 래핑이 필요합니다:

const GITHUB: &'static str = "https://github.com";
html! {
    a href={ (GITHUB) "/lambda-fairy/maud" } { "Fork me on GitHub" }
}

7. 제어 구조

모든 제어 키워드는 @ 접두사를 사용합니다. bare if / for / match / let은 html! 내부에서 제어 흐름으로 인식되지 않습니다. 모든 분기에 중괄호가 필수입니다.

@if / @else if / @else

#[derive(PartialEq)]
enum Princess { Celestia, Luna, Cadance, TwilightSparkle }

let user = Princess::Celestia;
html! {
    @if user == Princess::Luna {
        h1 { "Super secret woona to-do list" }
        ul { li { "Evil laugh" } }
    } @else if user == Princess::Celestia {
        p { "Sister, please stop reading my diary." }
    } @else {
        p { "Nothing to see here; move along." }
    }
}

@if let도 작동합니다(조건은 일반 Rust 표현식이므로 let PAT = EXPR이 기본적으로 구문 분석됨):

let user = Some("Pinkie Pie");
html! {
    p {
        "Hello, "
        @if let Some(name) = user { (name) } @else { "stranger" }
        "!"
    }
}

@for

let names = ["Applejack", "Rarity", "Fluttershy"];
html! {
    ol {
        @for name in &names {
            li { (name) }
        }
    }
}

@while

파서 AST(WhileExpr)에는 존재하지만, 게시된 문서에는 없습니다. 문서화된 예제는 존재하지 않습니다. 구조는 @if와 일치하므로 @while cond { ... } 및 유추하여 @while let PAT = EXPR { ... }도 작동할 것으로 예상됩니다. 컴파일할 때까지 @while let은 확인되지 않은 것으로 취급하세요.

@match

html! {
    @match user {
        Princess::Luna => h1 { "Woona's here" },
        Princess::Celestia | Princess::Cadance => p { "A princess." },
        _ => p { "Nothing to see here; move along." }
    }
}

위의 @if에서 Princess 열거형과 user 바인딩을 재사용합니다. Match 팔은 파서 소스에 따라 전체 Rust 패턴을 허용합니다: or-패턴(A | B, 위에 표시됨), 바인딩, 범위, 구조 분해, 가드(pat if cond =>). 문서에는 기본적인 팔만 표시되므로 가드/or-패턴 팔은 컴파일하여 확인하세요.

@let

html! 내부에 변수를 선언하며, 루프 내부에서 가장 유용합니다. 유형 명시를 포함한 모든 Rust let 패턴을 허용합니다.

let names = ["Applejack", "Rarity", "Fluttershy"];
html! {
    @for name in &names {
        @let first_letter = name.chars().next().unwrap();
        p {
            "The first letter of " b { (name) } " is " b { (first_letter) } "."
        }
    }
}

8. 이스케이프와 원시 HTML

스플라이스와 텍스트는 기본적으로 이스케이프됩니다. Maud는 정확히 네 문자를 이스케이프합니다: &, <, >, ". 작은따옴표는 이스케이프되지 않고 통과합니다.

maud::PreEscaped로 이스케이프를 우회하면 내부 값을 그대로 렌더링합니다.

use maud::PreEscaped;
html! {
    "<script>alert(\"XSS\")</script>"                // &lt;script&gt;...
    (PreEscaped("<script>alert(\"XSS\")</script>"))  // <script>...
}

이미 위생 처리된 콘텐츠만 래핑하세요. 신뢰할 수 없는 입력에 PreEscaped를 사용하면 XSS 구멍이 됩니다. PreEscaped에 대한 AsRef<str> 바운드 제한은 0.26.0에서 제거되어 이제 더 넓은 범위의 내부 유형을 래핑합니다.


9. 사용자 정의 렌더링과 구성

Render 트레이트

maud::Render 구현을 작성하여 Maud에 사용자 정의 유형을 스플라이스하는 방법을 알려줍니다. 트레이트에는 두 개의 메서드가 있으며, 둘 다 서로를 호출하는 기본 구현이 있습니다:

pub trait Render {
    fn render(&self) -> Markup {
        let mut buffer = String::new();
        self.render_to(&mut buffer);
        PreEscaped(buffer)
    }

    fn render_to(&self, buffer: &mut String) {
        buffer.push_str(&self.render().into_string());
    }
}

적어도 하나를 재정의하세요. 둘 다 재정의하지 않으면 각 기본 구현이 서로를 호출하여 무한 재귀가 발생합니다. 간단한 경우 render를 재정의하세요. 중간 Markup 없이 여러 조각을 추가하는 등 출력 버퍼에 직접 쓰려면 render_to를 재정의하세요.

render_to 재정의는 자체 이스케이프를 담당합니다. 버퍼에 원시 쓰기는 자동으로 이스케이프되지 않습니다. 내부에서 이스케이프가 필요하다면 maud::Escaper를 사용하세요. 쓰면서 이스케이프합니다.

render/render_to는 동기 전용입니다. 비동기 Render 트레이트나 스트리밍 변형은 없습니다. 비동기 데이터(DB 또는 HTTP 호출)는 html!에 진입하기 전에 값으로 해결하고 해결된 값을 스플라이스하세요.

컴포넌트는 단지 함수입니다

부분 또는 컴포넌트는 Markup을 반환하는 일반 함수입니다. 호출을 스플라이스하여 구성합니다.

use maud::{html, Markup, DOCTYPE};

fn header(title: &str) -> Markup {
    html! {
        head { title { (title) } }
    }
}

fn page(title: &str, body: Markup) -> Markup {
    html! {
        (DOCTYPE)
        html {
            (header(title))
            body { (body) }
        }
    }
}

// 사용법
let markup = page("Home", html! { h1 { "Hello" } });

Markup 값을 스플라이스해도 다시 이스케이프되지 않으므로 컴포넌트 중첩은 안전합니다. Maud는 현재 RenderOnce 트레이트를 내보내지 않습니다(0.8.0부터 0.15.0까지 있었으나 2017-01-26에 제거됨). horrorshow의 여전히 존재하는 RenderOnce 트레이트와 이름을 혼동하지 마세요.


10. 함정과 주의사항

각 항목은 해당 섹션에 인라인으로 표시되어 있습니다. §11에서 통합합니다. 색인:

  • 빈 요소 ;는 자체 닫힘 태그가 아님; 제어 흐름에는 @ 필요(모든 분기는 중괄호); §4, §7
  • [cond] (토글) vs attr=[Option<T>] (Some일 때만 값); 속성 연결에는 { } 필요; §5, §6
  • 스플라이스는 기본적으로 이스케이프됨; PreEscaped는 신뢰할 수 없는 입력에 XSS 구멍; & < > "만 이스케이프됨(작은따옴표는 아님); §6, §8
  • Rust 2021 # 공백(input #pinkie;); §5
  • Render에서 두 메서드 중 어느 것도 재정의하지 않으면 영원히 재귀; render_to 재정의는 자체 이스케이프를 수행함(maud::Escaper); §9
  • render/render_to는 동기 전용; 비동기 Render 트레이트 없음, 스트리밍 변형 없음; html! 전에 비동기 값 해결(§9)
  • Markup에는 Display 구현이 없음(.into_string() / maud::display(x) 사용); 템플릿 파일 없음, html_to! 없음; §2
  • "타입 검사됨"은 Rust 스플라이스 유형만 다룸; HTML5 태그/속성 검증 없음, 콘텐츠 모델 중첩 검사 없음(§4)

11. 빠른 참조 카드

값:            html! { ... } -> Markup          문자열 얻기: .into_string()
Doctype:       (DOCTYPE)                          -> <!DOCTYPE html>
요소:          p { "text" 중첩 {

관련 스킬