본문 바로가기

React

React 앱에서 마크다운(Markdown) 렌더링 구현하기

마크다운은 개발 문서, 블로그, 릴리즈 노트 등을 빠르게 작성하기에 최적의 포맷입니다. React 앱에서 마크다운을 안전하고 보기 좋게 렌더링하는 방법을 실무 중심으로 정리합니다.

1. 어떤 라이브러리를 쓸까요?

react-markdown은 마크다운을 React 컴포넌트로 변환해주는 표준 선택지입니다. GitHub-Flavored Markdown은 remark-gfm으로 지원하며, 제목 앵커, 자동 링크, 보안은 rehype 플러그인들로 강화합니다. 코드 하이라이트는 react-syntax-highlighter 또는 rehype-highlight를 사용할 수 있습니다.

2. 설치

// 필수 라이브러리 설치
// npm i react-markdown remark-gfm rehype-slug rehype-autolink-headings
// 코드 하이라이트용 선택
// npm i react-syntax-highlighter
// 보안 강화를 원하면
// npm i rehype-sanitize

3. 최소 구현: 기본 렌더러

import React from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';

export function MarkdownRenderer({ markdown }) {
  return (
    <ReactMarkdown
      remarkPlugins={[remarkGfm]}
      rehypePlugins={[rehypeSlug, [rehypeAutolinkHeadings, { behavior: 'wrap' }]]}
    >
      {markdown}
    </ReactMarkdown>
  );
}

위 구현은 GFM 표와 체크박스, 제목에 앵커 링크를 제공합니다. raw HTML을 허용하지 않으므로 기본적으로 안전합니다.

4. 코드 블록 하이라이트 추가

import React, { useMemo } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import rehypeSlug from 'rehype-slug';
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
import js from 'react-syntax-highlighter/dist/esm/languages/hljs/javascript';
import ts from 'react-syntax-highlighter/dist/esm/languages/hljs/typescript';
import jsonLang from 'react-syntax-highlighter/dist/esm/languages/hljs/json';
import oneDark from 'react-syntax-highlighter/dist/esm/styles/hljs/atom-one-dark';

SyntaxHighlighter.registerLanguage('js', js);
SyntaxHighlighter.registerLanguage('javascript', js);
SyntaxHighlighter.registerLanguage('ts', ts);
SyntaxHighlighter.registerLanguage('json', jsonLang);

export function MarkdownRenderer({ markdown }) {
  const md = useMemo(() => markdown || '', [markdown]);
  return (
    <ReactMarkdown
      remarkPlugins={[remarkGfm]}
      rehypePlugins={[rehypeSlug, [rehypeAutolinkHeadings, { behavior: 'wrap' }]]}
      components={{
        code({ inline, className, children, ...props }) {
          const match = /language-(\w+)/.exec(className || '');
          const lang = match?.[1] || 'plaintext';
          if (!inline) {
            return (
              <SyntaxHighlighter language={lang} style={oneDark} PreTag="div" {...props}>
                {String(children).replace(/\n$/, '')}
              </SyntaxHighlighter>
            );
          }
          return <code className={className} {...props}>{children}</code>;
        },
      }}
    >
      {md}
    </ReactMarkdown>
  );
}

마크다운의 ```js 처럼 언어 힌트를 사용하면 자동으로 하이라이트가 적용됩니다.

5. 링크와 이미지 UX 개선

// 외부 링크는 새 창, 이미지 지연 로딩
components={{
  a({ href, children, ...props }) {
    const isExternal = href && /^https?:\/\//.test(href);
    return (
      <a
        href={href}
        target={isExternal ? '_blank' : undefined}
        rel={isExternal ? 'noopener noreferrer' : undefined}
        {...props}
      >{children}</a>
    );
  },
  img({ src, alt, ...props }) {
    return <img src={src || ''} alt={alt || ''} loading="lazy" {...props} />;
  }
}}

사용자 경험과 SEO를 함께 고려해 외부 링크 보안 속성을 추가하고 이미지 지연 로딩을 적용합니다.

6. 보안과 XSS 대응

react-markdown은 기본적으로 raw HTML을 렌더링하지 않아 안전합니다. 필요 시 HTML을 허용하려면 rehype-raw를 쓰게 되는데, 이 경우 반드시 rehype-sanitize로 허용 목록 기반 정화를 적용해야 합니다.

// HTML을 일부 허용해야 하는 경우: sanitize 스키마 확장 예시
import rehypeRaw from 'rehype-raw';
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';

const schema = {
  ...defaultSchema,
  attributes: {
    ...defaultSchema.attributes,
    code: [...(defaultSchema.attributes.code || []), ['className', 'language-*']],
    span: [...(defaultSchema.attributes.span || []), ['className', 'token *']],
  },
};

<ReactMarkdown
  remarkPlugins={[remarkGfm]}
  rehypePlugins={[
    rehypeSlug,
    [rehypeAutolinkHeadings, { behavior: 'wrap' }],
    rehypeRaw,
    [rehypeSanitize, schema],
  ]}
>{markdown}</ReactMarkdown>

가능하면 rehype-raw 없이 운영하는 것이 가장 안전합니다. 외부 입력을 렌더링할 때는 항상 허용 목록 접근을 유지합니다.

7. 마크다운 소스 불러오기

import React, { useEffect, useState } from 'react';
import { MarkdownRenderer } from './MarkdownRenderer';

export function Article() {
  const [md, setMd] = useState('');
  useEffect(() => {
    fetch('/docs/readme.md')
      .then((r) => r.text())
      .then(setMd)
      .catch(() => setMd('# 로드 실패'));
  }, []);
  return <MarkdownRenderer markdown={md} />;
}

정적 파일, CMS, GitHub API 등 다양한 소스에서 텍스트를 불러와 렌더링할 수 있습니다.

8. SEO와 SSR 고려

검색엔진 크롤러가 콘텐츠를 인덱싱하려면 SSR이 유리합니다. Next.js에서 react-markdown은 서버 컴포넌트에서도 동작하지만, 코드 하이라이트처럼 브라우저 의존성이 있는 기능은 클라이언트 컴포넌트로 분리하거나 동적 로딩을 권장합니다.

// Next.js에서 하이라이트만 클라이언트로 분리 예시 (app router)
'use client';
import dynamic from 'next/dynamic';
const SyntaxHighlighter = dynamic(
  () => import('react-syntax-highlighter').then((m) => m.Light),
  { ssr: false }
);
// 나머지 react-markdown은 서버에서 렌더링하여 SEO를 확보합니다.

제목 앵커, 메타 태그, 구조화 데이터 등과 함께 서버 렌더링을 적용하면 블로그 SEO에 효과적입니다.

9. 성능 최적화 팁

큰 문서를 렌더링할 때는 하이라이트 라이브러리와 스타일을 필요한 언어만 등록하고, React.lazy 또는 dynamic import로 지연 로딩합니다. 마크다운 문자열이 자주 변하지 않으면 useMemo로 재연산을 줄이고, 매우 긴 목록은 가상화 라이브러리를 고려합니다.

10. 실전 체크리스트

외부 입력인지 여부를 결정하고 보안 정책을 설정합니다. 코드 블록 언어 힌트를 표준화합니다. 링크와 이미지에 UX와 보안 속성을 적용합니다. SSR 환경에서 동작을 검증합니다. 큰 문서 성능과 번들 크기를 모니터링합니다.

11. 샘플 데이터로 빠르게 검증

const sample = `# 마크다운 렌더링 테스트\n\n- **굵게**와 _기울임_\n- 코드:\n\n\`inline code\`\n\n\n\`\`\`js\nconsole.log('하이라이트');\n\`\`\`\n`;

// 렌더링
<MarkdownRenderer markdown={sample} />

위 샘플로 GFM, 제목 앵커, 코드 하이라이트가 기대대로 동작하는지 즉시 확인합니다.

12. 마무리

react-markdown을 기반으로 GFM, 앵커, 하이라이트, 보안까지 구성하면 실무에서도 바로 활용 가능한 마크다운 렌더링 환경을 갖출 수 있습니다. SSR을 병행하고 성능을 다듬으면 문서형 콘텐츠의 SEO와 사용자 경험이 모두 개선됩니다.