본문으로 건너뛰기

© 2026 Molayo

GitHub요약2026. 06. 15. 09:25

humanspeak/svelte-markdown

요약

Humanspeak에서 출시한 @humanspeak/svelte-markdown은 Svelte 5와 TypeScript를 지원하는 강력한 마크다운 렌더러입니다. 특히 LLM 스트리밍 출력에 최적화되어 있으며, 보안이 강화된 HTML 파싱과 커스텀 컴포넌트 렌더링 기능을 제공합니다.

핵심 포인트

  • Svelte 5 runes 및 TypeScript 완벽 지원
  • LLM 스트리밍 모드 지원으로 점진적 렌더링 최적화
  • XSS 방지를 위한 안전한 HTML 파싱 기본 제공
  • 커스텀 HTML 태그를 사용자 정의 컴포넌트로 매핑 가능
  • 지능형 토큰 캐싱을 통한 빠른 재렌더링 성능

TypeScript 지원을 포함한 Svelte용 강력하고 커스터마이징 가능한 마크다운 렌더러 (markdown renderer). Pablo Berganza의 기존 svelte-markdown 패키지의 후속작으로 제작되었으며, 현재 Humanspeak, Inc.에서 유지 관리 및 개선하고 있습니다.

  • 🔒 HTMLParser2를 통한 안전한 HTML 파싱 (Secure HTML parsing): 내장된 XSS 기본 설정 제공 (프로토콜 허용 목록, on* 핸들러 제거)
  • 🚀 Marked를 통한 전체 마크다운 문법 지원
  • 💪 엄격한 타이핑을 포함한 완전한 TypeScript 지원
  • 🔄 Svelte 5 runes 호환성
  • ✂️ 인라인 스니펫 오버라이드 (Inline snippet overrides) — 별도의 파일 없이 렌더러 커스터마이징 가능
  • 🎨 커스터마이징 가능한 컴포넌트 렌더링 시스템
  • ♿ WCAG 2.1 접근성 준수
  • 🎯 헤더를 위한 GitHub 스타일의 슬러그 (slug) 생성
  • 🧪 포괄적인 테스트 커버리지 (vitest 및 playwright)
  • 🧩 extensions 프롭을 통한 일급 마크드 확장 지원 (예: KaTeX 수학식, 알림)
  • ⚡ 지능형 토큰 캐싱 (50-200배 빠른 재렌더링)
  • 📡 점진적 렌더링을 지원하는 LLM 스트리밍 모드 (업데이트당 평균 ~1.6ms)
  • 🖼️ 페이드인 애니메이션이 포함된 스마트 이미지 지연 로딩 (lazy loading)

npm i -S @humanspeak/svelte-markdown

또는 선호하는 패키지 매니저를 사용하세요:

pnpm add @humanspeak/svelte-markdown
yarn add @humanspeak/svelte-markdown
<script lang="ts">
import SvelteMarkdown from '@humanspeak/svelte-markdown'
const source = `
# This is a header
...

Claude Code, Codex, 에이전틱 워크플로우 (agentic workflows)와 같은 현대적인 AI 코딩 에이전트들은 더 풍부한 출력(디자인 목업, 대시보드, 보고서, 인터랙티브 아티팩트)을 위해 마크다운과 함께 HTML을 점점 더 많이 생성합니다. @humanspeak/svelte-markdown은 바로 이를 위해 구축되었습니다:

단일 소스 내 마크다운 + HTML 혼합 — 에이전트가 표준 마크다운과 풍부한 HTML(표, SVG, 커스텀 요소)을 두 번째 렌더러 없이 교차하여 사용할 수 있습니다.
기본적으로 활성화된 XSS 방지javascript: URL 및 on* 핸들러가 렌더링 전 에이전트 출력에서 제거되며, 별도의 옵션 설정이 필요하지 않습니다 (보안 섹션 참조).
스트리밍 인식 살균 (Streaming-aware sanitization)streaming 시...

enabled되면 각 토큰이 방출될 때마다 살균(sanitized)됩니다. 태그 중간의 부분적인 요소들은 형식이 올바르게 갖춰질 때까지 버퍼에 저장되므로, LLM으로부터 생성되는 점진적인 HTML이 깜빡임 없이 렌더링됩니다.

커스텀 HTML 태그 지원 (Custom HTML tag support)<tool-call>, <thinking>와 같은 의미론적 마크업(semantic markup)이나 직접 설계한 디자인 시스템 태그를 renderers.html을 통해 사용자 정의 컴포넌트로 라우팅할 수 있습니다 (커스텀 HTML 태그 참조).

<script lang="ts"> import SvelteMarkdown from '@humanspeak/svelte-markdown'
import type { StreamingChunk } from '@humanspeak/svelte-markdown'
let markdown: { writeChunk: (chunk: StreamingChunk) => void } | undefined
...

HTML이 왜 에이전트의 일반적인 출력 형식이 되었는지에 대한 배경 지식은 Thariq의 포스트인 'Using Claude Code: The Unreasonable Effectiveness of HTML'을 참조하세요. 전체 스트리밍 API (오프셋 청크, 리셋, 웹소켓 패턴)에 대해서는 아래의 'LLM 스트리밍 (LLM Streaming)' 섹션을 참조하세요.

이 패키지는 TypeScript로 작성되었으며 전체 타입 정의(type definitions)를 포함합니다:

import type {
Renderers,
Token,
...

렌더러 맵(renderer maps)과 헬퍼 키(helper keys)를 임포트하여 동작을 선택적으로 재정의할 수 있습니다.

import SvelteMarkdown, {
// Maps
defaultRenderers, // markdown renderer map
...

참고 사항

rendererKeys는 의도적으로 html을 제외합니다. HTML 태그 재정의에는 htmlRendererKeys를 사용하세요. 통과(pass-through) 폴백(fallback) 전략을 원하는 경우 UnsupportedUnsupportedHTML을 사용할 수 있습니다.

이러한 헬퍼들을 사용하면 거대한 맵을 직접 작성하지 않고도 렌더러의 일부 집합만 허용하거나 일부 집합만 제외하는 작업을 쉽게 수행할 수 있습니다.

HTML 헬퍼 (HTML helpers)

  • buildUnsupportedHTML(): 모든 HTML 태그가 UnsupportedHTML을 사용하는 맵을 반환합니다.
  • allowHtmlOnly(allowed): 제공된 태그만 활성화하며, 나머지는 UnsupportedHTML을 사용합니다.
    • 'strong'과 같은 태그 이름이나, 커스텀 컴포넌트를 연결하기 위한 ['div', MyDiv]와 같은 튜플(tuple)을 허용합니다.
  • excludeHtmlOnly(excluded, overrides?): 나열된 태그만 비활성화(UnsupportedHTML로 매핑)하며, 제외되지 않은 태그에 대해서는 튜플을 사용하여 선택적으로 재정의(overrides)할 수 있습니다.

마크다운 헬퍼 (Markdown helpers, 비-HTML)
buildUnsupportedRenderers()

: html을 제외한 모든 마크다운 렌더러 (markdown renderers)가 Unsupported를 사용하도록 하는 맵 (map)을 반환합니다.

.allowRenderersOnly(allowed)

: 제공된 마크다운 렌더러 키 (markdown renderer keys)만 활성화하며, 나머지는 Unsupported를 사용합니다.

  • 커스텀 컴포넌트 (custom components)를 플러그인하기 위해 'paragraph'와 같은 키 또는 ['paragraph', MyParagraph]와 같은 튜플 (tuple)을 허용합니다.

  • excludeRenderersOnly(excluded, overrides?)와 같은 키를 허용합니다:

: 나열된 마크다운 렌더러 키만 비활성화하며, 튜플을 사용하여 비활성화되지 않은 키에 대한 선택적 오버라이드 (overrides)를 제공할 수 있습니다.

HTML 헬퍼 (HTML helpers)는 전체 renderers 맵의 html 키 내부에서 사용될 HtmlRenderers 맵을 반환합니다. 이들은 그 자체로 전체 renderers 객체 (object)를 교체하지 않습니다.

기본: 마크다운 기본값은 유지하고, 몇 개의 HTML 태그만 허용합니다 (나머지는 UnsupportedHTML이 됩니다):

import SvelteMarkdown, { defaultRenderers, allowHtmlOnly } from '@humanspeak/svelte-markdown'
const renderers = {
...defaultRenderers, // 마크다운 기본값 유지
...

다른 태그들은 기본값으로 허용하면서, 특정 태그 하나에 대해서만 커스텀 컴포넌트를 허용합니다:

import SvelteMarkdown, { defaultRenderers, allowHtmlOnly } from '@humanspeak/svelte-markdown'
const renderers = {
...defaultRenderers,
...

몇 개의 HTML 태그만 제외합니다; 다른 모든 HTML 태그는 기본값으로 유지합니다:

import SvelteMarkdown, { defaultRenderers, excludeHtmlOnly } from '@humanspeak/svelte-markdown'
const renderers = {
...defaultRenderers,
...

모든 HTML을 빠르게 비활성화합니다 (마크다운 기본값은 변경되지 않음):

import SvelteMarkdown, { defaultRenderers, buildUnsupportedHTML } from '@humanspeak/svelte-markdown'
const renderers = {
...defaultRenderers,
...

기본값과 함께 paragraph와 link만 허용하고 나머지는 비활성화합니다:

import { allowRenderersOnly } from '@humanspeak/svelte-markdown'
const md = allowRenderersOnly(['paragraph', 'link'])

link만 제외합니다; 나머지는 기본값으로 유지합니다:

import { excludeRenderersOnly } from '@humanspeak/svelte-markdown'
const md = excludeRenderersOnly(['link'])

모든 마크다운 렌더러 (html 제외)를 빠르게 비활성화합니다:

import { buildUnsupportedRenderers } from '@humanspeak/svelte-markdown'
const md = buildUnsupportedRenderers()

SvelteMarkdown을 위해 renderers 내의 두 맵(map)을 모두 결합할 수 있습니다.

<script lang="ts">
import SvelteMarkdown, { allowRenderersOnly, allowHtmlOnly } from '@humanspeak/svelte-markdown'
const renderers = {
// 최소한의 마크다운 세트만 허용
...

다음은 TypeScript 지원을 포함한 커스텀 렌더러 (custom renderer)의 전체 예시입니다:

<script lang="ts">
import type { Snippet } from 'svelte'
interface Props {
children?: Snippet
...

다른 렌더러를 확장하고 싶다면 renderers 폴더 내부에서 기본 구현 (default implementation)을 확인해 주세요. 기능 추가를 원하신다면 언제든지 이슈 (issue)를 생성해 주세요!

클래스 추가, 속성 (attribute) 변경, div로 감싸기 등 간단한 수정의 경우, 별도의 컴포넌트 파일을 만드는 대신 Svelte 5 스니펫 (snippets)을 사용하여 인라인으로 렌더러를 재정의할 수 있습니다:

<script lang="ts">
import SvelteMarkdown from '@humanspeak/svelte-markdown'
const source = '# Hello\n\nA paragraph with [a link](https://example.com).'
</script>
...

컨테이너 렌더러 (Container renderers) (paragraph, heading, blockquote, list 등)는 중첩된 콘텐츠를 위한 children 스니펫을 받습니다.

리프 렌더러 (Leaf renderers) (code, image, hr, br)는 데이터 프롭스 (data props)만 받으며, children은 받지 않습니다.

우선순위 (Precedence): 스니펫 (snippet) > 컴포넌트 렌더러 (component renderer) > 기본값 (default). 만약 스니펫과 renderers.paragraph 컴포넌트가 모두 제공되면, 스니펫이 우선합니다.

HTML 태그 스니펫은 마크다운 렌더러 이름과의 충돌을 방지하기 위해 html_ 접두사를 사용합니다:

<SvelteMarkdown {source}>
{#snippet html_div({ attributes, children })}
<div class="custom-wrapper" {...attributes}>{@render children?.()}</div>
...

모든 HTML 스니펫은 { attributes?: Record<string, any>, children?: Snippet }라는 통일된 프롭스 인터페이스 (props interface)를 공유합니다.

<click>, <tooltip>과 같은 임의의 (비표준) HTML 태그를 렌더링할 수 있습니다.

, 또는 태그 이름에 대한 렌더러(renderer)나 스니펫(snippet)을 제공함으로써 임의의 커스텀 엘리먼트(custom element)를 렌더링할 수 있습니다. 파싱 파이프라인(parsing pipeline)은 모든 태그 이름을 허용하므로, SvelteMarkdown에게 이를 어떻게 렌더링할지만 알려주면 됩니다.

컴포넌트 렌더러(Component renderer) 방식:

<script lang="ts"> import SvelteMarkdown from '@humanspeak/svelte-markdown'
import ClickButton from './ClickButton.svelte'
const source = '<click>Click Me</click>'
...

스니펫 오버라이드(Snippet override) 방식:

<SvelteMarkdown source={'<click data-action="submit">Click Me</click>'}>
{#snippet html_click({ attributes, children })}
<button {...attributes} class="custom-btn">{@render children?.()}</button>
...

두 방식 모두 모든 태그 이름에 대해 작동합니다. 컴포넌트 렌더러와 스니펫 오버라이드가 모두 제공될 경우, 스니펫 오버라이드가 우선순위를 갖습니다.

extensions 프롭(prop)을 통해 marked 확장 기능(extensions)을 사용할 수 있습니다. SvelteMarkdown@humanspeak/svelte-markdown/extensions 서브패스(subpath)를 통해 KaTeX, Mermaid, GitHub 스타일 알림(alerts), 각주(footnotes)를 위한 퍼스트 클래스(first-class) 확장 기능을 제공하며, 별도의 서드파티 패키지가 필요하지 않습니다. 서드파티 확장 기능도 여전히 작동합니다. 컴포넌트가 내부적으로 토크나이저(tokenizer) 등록을 처리하므로, 사용자는 커스텀 토큰 타입에 대한 렌더러(renderer)만 제공하면 됩니다.

이 패키지에는 내장된 markedKatexKatexRenderer 헬퍼(helpers)가 포함되어 있습니다. katex를 선택적 피어 의존성(optional peer dependency)으로 설치하고 해당 CSS를 로드하세요:

npm install katex

기본 구분자 세트 (KaTeX 자체의 auto-render 기본값과 동일):

구분자 쌍레벨 (Level)displayMode
\(...\)인라인 (inline)false
\[...\] (단독 행)블록 (block)true
$$...$$ (단독 행)블록 (block)true
\begin{equation}...\end{equation} 및 기타 AMS 환경블록 (block)true

단일 달러 인라인 ($x^2$)은 기본적으로 꺼져(off) 있습니다. KaTeX 자체도 $5,000과 같은 통화 문자열과의 충돌을 피하기 위해 이를 자동 렌더링(auto-render)에서 제외합니다. 이를 활성화하려면 { singleDollarInline: true }를 전달하세요. 공백 경계 규칙(whitespace-bounded rule)을 사용하므로 통화 문자열은 여전히 매칭되지 않습니다.

컴포넌트 렌더러(Component renderer) 방식:

<script lang="ts">
import SvelteMarkdown from '@humanspeak/svelte-markdown'
import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'
import { markedKatex, KatexRenderer } from '@humanspeak/svelte-markdown/extensions'
...

KatexRenderer

throwOnError: false를 하드코딩(hardcodes)합니다.

따라서 단일 잘못된 수식(malformed expression)이 에러를 발생시키는 대신 색이 입혀진 에러 스팬(error span)으로 렌더링됩니다. 더 엄격한 동작이 필요한 경우, inlineKatexblockKatex 키에 대해 직접 컴포넌트를 제공하세요.

스니펫 재정의(Snippet override) 방식 (별도의 컴포넌트 파일이 필요 없음):

<script lang="ts">
import SvelteMarkdown from '@humanspeak/svelte-markdown'
import { markedKatex } from '@humanspeak/svelte-markdown/extensions'
import katex from 'katex'
...

이 패키지에는 Mermaid 다이어그램 지원을 위한 내장된 markedMermaidMermaidRenderer 헬퍼(helpers)가 포함되어 있습니다. 선택적 피어 의존성(optional peer dependency)으로 mermaid를 설치하세요:

npm install mermaid

그 다음, 보일러플레이트(boilerplate) 없이 내장된 헬퍼를 사용하면 됩니다:

<script lang="ts">
import SvelteMarkdown from '@humanspeak/svelte-markdown'
import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'
import { markedMermaid, MermaidRenderer } from '@humanspeak/svelte-markdown/extensions'
...

markedMermaid()는 ````mermaid코드 블록을 커스텀 토큰(custom tokens)으로 변환하는 의존성 없는(zero-dependency) 토크나이저(tokenizer)입니다.MermaidRenderer`는...

<SvelteMarkdown source={markdown} extensions={[markedMermaid()]}>
{#snippet mermaid(props)}
<div class="my-diagram-wrapper">
<MermaidRenderer text={props.text} />
</div>
{/snippet}
</SvelteMarkdown>

Mermaid 렌더링은 비동기(async) 방식이므로, 스니펫은 MermaidRenderer에 위임합니다...


<script lang="ts"> import SvelteMarkdown from '@humanspeak/svelte-markdown'
import type { RendererComponent, Renderers } from '@humanspeak/svelte-markdown'
import { markedAlert, AlertRenderer } from '@humanspeak/svelte-markdown/extensions'
const source = `
>
[!NOTE]
Useful information that users should know.
[!WARNING]
Urgent info that needs immediate attention.
`
interface AlertRenderers extends Renderers {
alert: RendererComponent
}
const renderers: Partial<AlertRenderers> = {
alert: AlertRenderer
}
script>
<SvelteMarkdown {source} extensions={[markedAlert()]} {renderers} />

`AlertRenderer``

...

<SvelteMarkdown source={markdown} extensions={[markedAlert()]}>
{#snippet alert(props)}
<div class="my-alert my-alert-{props.alertType}">
<strong>{props.alertType}</strong>
<p>{props.text}</p>
</div>
{/snippet}
{/SvelteMarkdown}>

각주 참조 및 정의에 대한 내장 지원을 제공합니다. 각주 참조([^id])

AI 자동 생성 콘텐츠

본 콘텐츠는 GitHub AI Coding Assistants의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0