Pluto Design System
Components

Chat Markdown

채팅 메시지의 markdown 렌더 가이드 — 별도 패키지 @fluxloop-ai/pds-markdown

ChatUserMessage · ChatAssistantMessage 는 모두 renderMarkdown?: (text: string) => ReactNode slot 을 받는다. PDS 자체는 markdown 렌더러 의존을 가지지 않고 — 권장 구현체를 별도 패키지 @fluxloop-ai/pds-markdown 으로 분리해 제공한다.

왜 별도 패키지인가

  • pds-uireact-markdown/remark-gfm 같은 무거운 deps 를 강제하지 않도록.
  • chat 안 쓰는 컨슈머(예: admin 도구·문서 뷰어)는 markdown 만 가져다 쓸 수 있도록.
  • 컨슈머 앱마다 자체 렌더러(Anthropic SDK 도우미, 자체 Markdown 컴포넌트, streaming 친화 변형 등) 를 끼울 수 있도록.

pds-iconspds-ui 와 분리된 것과 동일한 원칙.

설치

pnpm add @fluxloop-ai/pds-markdown

내부 의존성: react-markdown ^9, remark-gfm ^4 (GFM — 테이블·체크박스·autolink 등). React 19 peer.

사용

1) renderMarkdown 헬퍼 — slot 에 그대로 꽂기

가장 흔한 케이스. (text) => ReactNode 형태라 그대로 prop 으로 전달.

import { ChatAssistantMessage } from "@fluxloop-ai/pds-ui/components/chat-assistant-message";
import { renderMarkdown } from "@fluxloop-ai/pds-markdown";

<ChatAssistantMessage content={text} renderMarkdown={renderMarkdown} />

2) Markdown 컴포넌트 — 직접 렌더

slot 외 컨텍스트(예: 단독 본문 영역) 에서 markdown 을 그릴 때.

import { Markdown } from "@fluxloop-ai/pds-markdown";

<Markdown>{`**굵게** 와 \`코드\` 가 들어간 텍스트`}</Markdown>

루트에 pds-markdown 클래스가 붙으므로 앱 측에서 typography 오버라이드 가능.

전 element 가 어떻게 그려지는지 시각 결과는 ChatAssistantMessage 페이지의 kitchen-sink 데모 참고.

폴백 동작

renderMarkdown 미제공 시:

  • ChatUserMessagewhitespace-pre-wrap 적용된 plain text
  • ChatAssistantMessage → 동일하게 whitespace-pre-wrap plain text

렌더러가 없어도 화면은 정상. 마크다운 문법(**, `, # 등) 이 그대로 노출되긴 하지만 깨지지는 않는다. user 메시지는 마크다운이 없는 경우가 많아 미주입 운영도 합리적이다.

보안

react-markdown 은 기본적으로 raw HTML 을 거부하고 mdast → hast 트리만 통과시킨다. assistant 출력처럼 신뢰할 수 없는 텍스트라도 XSS 위험이 낮다. raw HTML 을 의도적으로 허용하려면 rehype-raw 등을 추가해야 하지만 — chat 컨텍스트에서는 권장하지 않음.

커스터마이즈

Element 매핑

Markdown 컴포넌트는 components prop 으로 element 별 렌더 오버라이드를 받는다.

import { Markdown } from "@fluxloop-ai/pds-markdown";

<Markdown
  components={{
    a: ({ href, children }) => <a href={href} target="_blank" rel="noopener">{children}</a>,
    code: ({ children }) => <code className="bg-[var(--pds-fill-alternative)] px-[4px] rounded-[4px]">{children}</code>,
  }}
>
  {text}
</Markdown>

컨슈머 자체 렌더러 사용

@fluxloop-ai/pds-markdown 을 안 쓰고 컨슈머가 자체 함수를 만들어도 된다 (slot 시그니처만 지키면 됨).

function myRender(text: string) {
  return <MyMarkdown>{text}</MyMarkdown>;
}

<ChatAssistantMessage content={text} renderMarkdown={myRender} />

이 패턴은 다음 경우에 유용:

  • 앱이 이미 자체 markdown 렌더러(@assistant-ui/react-markdown 등) 를 가지고 있을 때
  • streaming 친화 처리(unfinished fence 보정 등) 가 필요할 때
  • 자체 syntax highlighter 와 한 함수에서 묶어야 할 때

확장 (향후)

PDS 본체 의존을 늘리지 않는 선에서 같은 패키지에 추가 예정인 것:

  • 코드 블록 syntax highlighting (shiki 또는 prism — 별도 sub-path export 로 옵트인)
  • 수식(KaTeX) 옵션
  • streaming 입력 보정 헬퍼

이 시점까지 컨슈머가 직접 plugin 을 끼우려면 react-markdownremarkPlugins / rehypePlugins 를 사용하면 된다 — 현재 Markdown 컴포넌트는 plugin slot 을 노출하지 않으므로, 임의 plugin 이 필요하면 react-markdown 을 직접 쓰는 자체 함수를 만들고 slot 에 주입하는 방식이 더 깔끔하다.