HAST와 ReactMarkdown 컴포넌트 매퍼
마크다운을 React로 렌더링하는 파이프라인에서, HAST는 HTML 구조를 트리로 표현한 중간 단계이고, ReactMarkdown의 컴포넌트 매퍼는 이 트리의 각 노드를 커스텀 React 컴포넌트로 대체하는 기능이다.
마크다운 렌더링 파이프라인
마크다운이 React 컴포넌트가 되기까지 세 단계를 거친다.
Markdown 문자열
│
▼ (remark)
MDAST (Markdown AST)
│
▼ (rehype)
HAST (HTML AST)
│
▼ (react-markdown)
React 엘리먼트
각 단계에서 플러그인으로 변환 과정에 개입할 수 있다. remark 플러그인은 MDAST를, rehype 플러그인은 HAST를 조작한다.
HAST 노드 구조
HAST(Hypertext Abstract Syntax Tree)는 HTML을 트리 구조로 표현한 것이다. 두 가지 주요 노드 타입이 있다.
Element — HTML 태그에 대응한다.
interface Element {
type: "element";
tagName: string; // "p", "a", "img" 등
properties: Properties; // { href: "...", className: [...] }
children: (Element | Text)[];
}
Text — 텍스트 콘텐츠에 대응한다.
interface Text {
type: "text";
value: string; // "Hello world", "\n" 등
}
마크다운 [링크](https://example.com)는 다음 HAST 구조가 된다.
Element(p)
└─ Element(a, href="https://example.com")
└─ Text("링크")
주의할 점은 HAST가 줄바꿈을 "\n" 값의 Text 노드로 남길 수 있다는 것이다. 노드를 검사할 때 공백 텍스트 노드를 필터링해야 정확한 결과를 얻을 수 있다.
ReactMarkdown 컴포넌트 매퍼
ReactMarkdown의 components prop으로 HTML 태그별 렌더링을 커스터마이징할 수 있다.
<ReactMarkdown
components={{
p: ({ children, node, ...props }) => <p {...props}>{children}</p>,
a: ({ href, children }) => <Link href={href}>{children}</Link>,
img: ({ src, alt }) => <Image src={src} alt={alt} />,
}}
>
{markdownContent}
</ReactMarkdown>
각 매퍼 함수는 해당 HTML 태그의 props와 함께 node prop을 받는다. node는 해당 요소의 HAST Element 객체다.
node prop 활용
node prop으로 HAST 구조를 직접 검사할 수 있다. 이를 통해 렌더링 시점에 조건부 로직을 추가할 수 있다.
function ParagraphWithLinkPreview({
children, node, ...props
}: React.HTMLAttributes<HTMLParagraphElement> & { node?: Element }) {
// node.children을 검사해서 단독 외부 링크인지 판별
const meaningful = node.children.filter(
(child) => !(child.type === "text" && child.value.trim() === "")
);
if (meaningful.length === 1 && meaningful[0].tagName === "a") {
const href = meaningful[0].properties?.href;
// 단독 링크면 프리뷰 카드를 추가로 렌더링
return (
<>
<p {...props}>{children}</p>
<LinkPreviewCard url={href} />
</>
);
}
return <p {...props}>{children}</p>;
}
이 방식의 장점은 AST를 변형하지 않는다는 것이다. HAST는 그대로 두고, 렌더링 단계에서만 추가 로직을 넣기 때문에 다른 rehype 플러그인과 충돌하지 않는다.
rehype 플러그인 vs 컴포넌트 매퍼
두 접근 방식은 개입 시점이 다르다.
| rehype 플러그인 | 컴포넌트 매퍼 | |
|---|---|---|
| 개입 시점 | HAST 변환 단계 | React 렌더링 단계 |
| 동작 | AST 노드 추가/수정/삭제 | 기존 노드를 커스텀 컴포넌트로 대체 |
| 장점 | 강력한 변환, 노드 구조 자체를 바꿀 수 있음 | 안전함, 다른 플러그인과 충돌 없음 |
| 단점 | 플러그인 체인 충돌 위험 | 노드 구조 변경 불가 |
| 적합한 경우 | 새 노드 삽입, 구조 변환 | 기존 노드의 렌더링 커스터마이징 |
"렌더링만 바꾸면 되는가, 아니면 구조 자체를 바꿔야 하는가"로 판단하면 된다. 기존 노드를 다른 방식으로 보여주기만 하면 되면 컴포넌트 매퍼가 안전하다. 새 노드를 삽입하거나 구조를 바꿔야 하면 rehype 플러그인이 필요하다.