React-Markdown 라이브러리에서 커스텀 컴포넌트를 렌더링하기 위해서
react-markdown과 rehype 조합이 사실상 표준처럼 쓰입니다.
하지만 이 라이브러리 조합엔 한 가지 불편한 점이 있습니다.
💡 rehype 플러그인은 타입 안정성이 거의 없습니다.
rehype 플러그인은 HTML AST를 직접 다루기 때문에,
각 노드의 형태나 속성을 TypeScript로 보장하기 어렵습니다.
즉, “이 노드에는 반드시 url이 있다”는 걸 코드로 증명할 수 없죠.
결국 런타임 에러나 타입 추론 불가능한 상황이 자주 생깁니다.
문제 상황
저는 react-markdown을 기반으로 커스텀 블록(예: Rechart, Mermaid, Callout 등)을 구현하고 있었어요.
rehype 플러그인은 HTML AST 노드를 파싱하고 변환해주지만,
이 과정에서 props의 타입을 전혀 알 수 없습니다.
예를 들어 <Callout bgColor="~" content="~" /> 같은 컴포넌트를
rehype 플러그인으로 파싱하면,
node.properties가 { [key: string]: any }로 나옵니다.
해결 아이디어
그래서 “AST 구조를 Zod로 검증하면서 타입을 동시에 확보하면 어떨까?”
라는 아이디어로 rehype-zod를 만들었습니다.
rehype-zod: rehype AST를 Zod 스키마로 검증하고,
타입 안전하게 props를 추론할 수 있도록 도와주는 rehype 플러그인 유틸입니다.
npm: https://www.npmjs.com/package/rehype-zod
GitHub: https://github.com/jongse7/rehype-zod
어떻게 동작하나요?
rehype-zod는 간단히 말해 AST 노드를 Zod 스키마로 통과시켜주는 래퍼입니다.
즉, AST → 안전한 컴포넌트 props 로 이어지는 파이프라인을 만들 수 있죠.
import ReactMarkdown from "react-markdown";
import rehypeRaw from "rehype-raw";
import { z } from "zod";
import zodComponents, { zodComponent } from "rehype-zod";
// 1) Define schemas
const calloutSchema = z.object({
type: z.enum(["info", "warn", "error"]).default("info"),
title: z.string().optional(),
description: z.string(),
});
const cardSchema = z.object({
title: z.string(),
badge: z.string().optional(),
content: z.string(),
});
// 2) Implement components with inferred props (example-only typing)
type CalloutProps = z.infer<typeof calloutSchema>;
const Callout = ({ type, title, description }: CalloutProps) => (
<div className={`callout callout-${type}`}>
{title && <strong>{title}</strong>}
<p>{description}</p>
</div>
);
type CardProps = z.infer<typeof cardSchema>;
const Card = ({ title, badge, content }: CardProps) => (
<div className="card">
<h3>
{title} {badge && <span className="badge">{badge}</span>}
</h3>
<p>{content}</p>
</div>
);
// 3) Map once: { tag: zodComponent(schema, component) }
const components = zodComponents({
callout: zodComponent(calloutSchema, Callout),
card: zodComponent(cardSchema, Card),
});
// 4) Use with react-markdown
export default function Article({ markdown }: { markdown: string }) {
return (
<ReactMarkdown rehypePlugins={[rehypeRaw]} components={components}>
{markdown}
</ReactMarkdown>
);
}
타입 안전성의 실제 효과
rehype-zod를 쓰면 다음과 같은 장점이 생깁니다:
✅ 컴파일 타임에 props 오류 잡기
✅ 런타임 검증 자동 수행
✅ data 객체 타입 자동 추론
✅ 플러그인 간 일관된 구조 유지
결국 “Markdown 렌더링도 타입 안전하게 하자”는 흐름을 만들 수 있었습니다.
마무리하며
혹시 react-markdown과 rehype를 쓰면서
“props 타입이 너무 any라서 답답하다” 느끼셨다면,
rehype-zod를 한 번 시도해보세요.