본문 바로가기
✨FRONTEND/📍React

React/Next.js 마크다운 적용하기

by 짱돌보리 2025. 3. 30.
728x90

React 마크다운 미리보기 적용하기👁️

React 프로젝트에서 글 작성 기능을 구현하면서 처음에는 단순히 textarea만 사용해 글을 입력받도록 만들었다.

 

하지만 마크다운 문법을 사용하는 글이라면 작성하면서 결과를 바로 확인할 수 있는 미리보기 기능이 있으면 훨씬 편리할 것 같았다.

 

특히 블로그 글이나 프로젝트 설명처럼 마크다운 문법(##, -, code block 등)을 사용하는 경우
작성자가 문법이 제대로 적용되는지 실시간으로 확인할 수 있어 가독성이 훨씬 좋아진다.

 

그래서 마크다운 실시간 미리보기 기능을 추가했다.

라이브러리 설치

pnpm add react-markdown remark-gfm remark-breaks

 

react-markdown

Markdown 문자열을 React 컴포넌트로 변환해주는 라이브러리

 

remark-gfm

GitHub Flavored Markdown(GFM)을 지원하는 플러그인

 

아래와 같은 문법이 가능하다.

  • 테이블
  • 체크박스
  • 취소선
  • 자동 링크

등...

 

remark-breaks

Markdown에서 Enter로 입력한 줄바꿈을 <br>로 변환해 일반 텍스트처럼 자연스럽게 줄바꿈이 보이도록 해주는 플러그인

실시간 마크다운 미리보기 적용하기

글 작성 화면에서는 왼쪽은 입력창, 오른쪽은 미리보기 영역으로 구성했다.

 

textarea에 입력되는 값을 formData의 content로 저장하고 그 값을 ReactMarkdown 컴포넌트에 전달해 실시간으로 렌더링한다.

  const [formData, setFormData] = useState({
    title: '',
    content: '',
    thumbnailUrl: '',
    techStack: [] as string[],
    category: null as string | null,
  })


  <div className="space-y-2">
  <Label htmlFor="content" className="font-semibold">
    내용
  </Label>

  <div className="flex w-full flex-col items-start justify-center gap-2 md:flex-row">
    <Textarea
      id="content"
      placeholder="내용을 자유롭게 입력하세요"
      className="min-h-[300px] resize-none leading-relaxed focus-visible:ring-1 md:w-1/2"
      value={formData.content}
      onChange={(e) =>
        setFormData({
          ...formData,
          content: e.target.value,
        })
      }
    />

    <div className="markdown-preview w-full md:w-1/2">
      <ReactMarkdown remarkPlugins={[remarkGfm, remarkBreaks]}>
        {formData.content}
      </ReactMarkdown>
    </div>
  </div>
</div>

게시글 조회(Read) 화면에서 사용하기

글을 작성할 때뿐만 아니라
등록된 게시글을 조회할 때도 Markdown 렌더링이 필요하다.

    <ReactMarkdown remarkPlugins={[remarkGfm, remarkBreaks]}>
      {description}
    </ReactMarkdown>

GitHub Markdown Style 기반 CSS

.markdown-preview {
  line-height: 1.6;
  color: #334155;
  font-size: 16px;
  word-wrap: break-word;
}

.markdown-preview h1,
.markdown-preview h2,
.markdown-preview h3 {
  padding-bottom: 0.3em;
  border-bottom: 1px solid #e2e8f0;
  margin-top: 24px;
  margin-bottom: 16px;
  font-weight: 700;
  color: #0f172a; 
}

.markdown-preview h1 {
  font-size: 2em;
}
.markdown-preview h2 {
  font-size: 1.5em;
}
.markdown-preview h3 {
  font-size: 1.25em;
}

/* 링크 스타일 */
.markdown-preview a {
  color: #3b82f6;
  text-decoration: none;
}

.markdown-preview a:hover {
  text-decoration: underline;
}

/* 코드 블록 스타일 */
.markdown-preview code {
  padding: 0.2em 0.4em;
  margin: 0;
  font-size: 85%;
  background-color: #f1f5f9; 
  border-radius: 6px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}

.markdown-preview pre {
  padding: 16px;
  overflow: auto;
  font-size: 85%;
  line-height: 1.45;
  background-color: #f8fafc; 
  border-radius: 8px;
  border: 1px solid #e2e8f0;
  margin-bottom: 16px;
}

.markdown-preview pre code {
  background-color: transparent;
  padding: 0;
  border-radius: 0;
}

/* 인용문 (Blockquote) */
.markdown-preview blockquote {
  padding: 0 1em;
  color: #64748b;
  border-left: 0.25em solid #3b82f6;
  margin: 16px 0;
}

/* 리스트 */
.markdown-preview ul {
  list-style-type: disc !important;
  padding-left: 1.5rem !important;
  margin-block: 1rem;
}

.markdown-preview ol {
  list-style-type: decimal !important;
  padding-left: 1.5rem !important;
  margin-block: 1rem;
}

.markdown-preview li {
  display: list-item;
}

/* 테이블  */
.markdown-preview table {
  width: 100%;
  border-collapse: collapse;
  margin-bottom: 16px;
}

.markdown-preview table th,
.markdown-preview table td {
  padding: 8px 13px;
  border: 1px solid #e2e8f0;
}

.markdown-preview table tr:nth-child(even) {
  background-color: #f8fafc;
}

.markdown-preview hr {
  margin-top: 2rem;
  margin-bottom: 2rem;
}

.markdown-preview p {
  word-break: break-word;
  margin-bottom: 1rem;
}

.markdown-preview li p {
  display: inline;
  white-space: normal;
}

⚠️ CSS 파일은 _app.tsx에서 불러오자

Next.js에서는 글로벌 CSS를 아무 곳에서나 import 하면 빌드 오류가 발생할 수 있다.

개발 중에는 문제가 없어 보이지만
빌드 시 다음과 같은 문제가 생길 수 있다.

Global CSS cannot be imported from files other than your Custom <App>