junyeokk
Blog
React·2026. 02. 18

HAST와 ReactMarkdown 컴포넌트 매퍼

마크다운을 React로 렌더링하는 파이프라인에서, HAST는 HTML 구조를 트리로 표현한 중간 단계이고, ReactMarkdown의 컴포넌트 매퍼는 이 트리의 각 노드를 커스텀 React 컴포넌트로 대체하는 기능이다.

마크다운 렌더링 파이프라인

마크다운이 React 컴포넌트가 되기까지 세 단계를 거친다.

plain
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 태그에 대응한다.

typescript
interface Element {
  type: "element";
  tagName: string;        // "p", "a", "img" 등
  properties: Properties; // { href: "...", className: [...] }
  children: (Element | Text)[];
}

Text — 텍스트 콘텐츠에 대응한다.

typescript
interface Text {
  type: "text";
  value: string;  // "Hello world", "\n" 등
}

마크다운 [링크](https://example.com)는 다음 HAST 구조가 된다.

plain
Element(p)
  └─ Element(a, href="https://example.com")
       └─ Text("링크")

주의할 점은 HAST가 줄바꿈을 "\n" 값의 Text 노드로 남길 수 있다는 것이다. 노드를 검사할 때 공백 텍스트 노드를 필터링해야 정확한 결과를 얻을 수 있다.

ReactMarkdown 컴포넌트 매퍼

ReactMarkdown의 components prop으로 HTML 태그별 렌더링을 커스터마이징할 수 있다.

tsx
<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 구조를 직접 검사할 수 있다. 이를 통해 렌더링 시점에 조건부 로직을 추가할 수 있다.

tsx
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 플러그인이 필요하다.

관련 문서