본문 바로가기

Web/React

React-Markdown에서 커스텀 컴포넌트 타입 안정성 챙기기

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를 한 번 시도해보세요.

👉 GitHub 바로가기