본문 바로가기
React

[React] 마크다운 프리뷰어 만들기

by teamnova 2025. 6. 23.
728x90

마크다운이란? 

 

<h1>안녕하세요!</h1>
<p>이것은 <strong>굵은 글씨</strong>와 <em>기울임꼴</em> 입니다.</p>
<ul>
  <li>목록 아이템</li>
  <li>또 다른 아이템</li>
</ul>

 

보시다시피 HTML은 태그가 많아 다소 번거롭고, 글 자체의 가독성을 해치기도 합니다. 글을 쓰기 위한 목적보다는 문서의 구조를 정의하는 데 더 초점이 맞춰져 있었죠.
이런 불편함을 해소하기 위해 2004년 존 그루버(John Gruber)는 "쉽게 쓰고, 쉽게 읽히고, 쉽게 HTML로 변환되는" 마크다운 언어를 만들었습니다.


쉬운 문법: #, *, - 와 같이 간단한 기호 몇 가지만으로도 서식을 표현할 수 있습니다.
# 제목 → <h1>제목</h1>
**굵게** → <strong>굵게</strong>
* 목록 → <ul><li>목록</li></ul>
높은 가독성: 마크다운 문법 자체가 읽기 쉽게 디자인되어 있습니다. HTML처럼 복잡한 태그 없이도 글의 구조를 파악하기 용이합니다.
플랫폼 호환성: 텍스트 파일이면 어디서든 작성하고 읽을 수 있으며, 다양한 도구를 통해 HTML, PDF 등 여러 형식으로 쉽게 변환할 수 있습니다.
간편한 협업: 글쓰기에 집중할 수 있어 블로그, README 파일, 개발 문서, 메시지 등 다양한 환경에서 널리 사용됩니다. GitHub의 README 파일이 대표적인 예시 입니다.

 

 

import React, { useState, useEffect, useRef } from 'react'; // useRef 추가
import './App.css';
import { marked } from 'marked';

function App() {
  const [markdown, setMarkdown] = useState(`
# 안녕하세요!

이것은 **마크다운 프리뷰어** 입니다.

## 사용법
1. 왼쪽 텍스트 영역에 마크다운을 입력하세요.
2. 오른쪽 영역에서 실시간 미리보기를 확인하세요.

### 코드 예시
\`\`\`javascript
function greet() {
  console.log("Hello, Markdown!");
}
greet();
\`\`\`

- 목록 아이템 1
- 목록 아이템 2

> 인용 블록입니다.
`);

  // textarea 엘리먼트에 접근하기 위한 ref
  const editorRef = useRef(null);
  const [editorHeight, setEditorHeight] = useState(0); // 초기 높이를 0으로 설정

  useEffect(() => {
    const updateHeight = () => {
      if (editorRef.current) {
        // 실제 textarea 엘리먼트의 높이를 viewport 높이에 맞춥니다.
        // padding 등 고려하여 약간의 여유를 줄 수도 있습니다.
        // 여기서는 간단하게 viewport 높이의 85%를 사용합니다.
        setEditorHeight(window.innerHeight * 0.85);
      }
    };

    // 컴포넌트가 마운트된 후에 최초 한 번 실행하여 초기 높이 설정
    updateHeight();

    // 창 크기 변경 이벤트 리스너 추가
    window.addEventListener('resize', updateHeight);

    // 컴포넌트가 언마운트될 때 이벤트 리스너 제거
    return () => {
      window.removeEventListener('resize', updateHeight);
    };
  }, []); // 컴포넌트가 처음 마운트될 때만 실행

  // textarea에 동적으로 스타일 적용
  const textareaStyle = {
    height: `${editorHeight}px`,
  };

  return (
    <div className="app-container">
      <div className="editor-pane">
        <textarea
          id="editor"
          ref={editorRef} // textarea 엘리먼트에 ref 연결
          value={markdown}
          onChange={(e) => setMarkdown(e.target.value)}
          style={textareaStyle}
        ></textarea>
      </div>
      <div className="preview-pane">
        <div
          id="preview"
          dangerouslySetInnerHTML={{ __html: marked(markdown) }}
        ></div>
      </div>
    </div>
  );
}

export default App;

 

 

body {
  margin: 0;
  font-family: sans-serif;
  background-color: #f0f0f0;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  overflow: hidden; /* 스크롤 방지 */
}

.app-container {
  display: flex;
  width: 90vw;
  height: 85vh;
  background-color: #fff;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
  border-radius: 8px;
  overflow: hidden;
}

.editor-pane,
.preview-pane {
  flex: 1;
  padding: 20px;
  overflow-y: auto; /* 내용이 길어지면 스크롤 */
  height: 100%;
  box-sizing: border-box;
}

.editor-pane {
  border-right: 1px solid #ccc;
  resize: horizontal; /* 사용자가 너비 조절 가능하게 */
  overflow: hidden; /* textarea 자체가 resize 되므로 필요 */
}

.editor-pane textarea {
  width: 100%;
  height: 100%;
  border: none;
  outline: none;
  font-size: 16px;
  line-height: 1.5;
  resize: none; /* textarea 자체 resize는 비활성화 (부모 pane이 resize) */
  padding: 10px;
  box-sizing: border-box;
  color: #333;
  background-color: #f9f9f9;
}

.preview-pane {
  background-color: #fff;
}

.preview-pane h1,
.preview-pane h2,
.preview-pane h3 {
  margin-top: 1.2em;
  margin-bottom: 0.5em;
}

.preview-pane h1 {
  font-size: 2em;
  border-bottom: 2px solid #ccc;
  padding-bottom: 0.3em;
}

.preview-pane h2 {
  font-size: 1.5em;
  border-bottom: 1px solid #eee;
}

.preview-pane code {
  font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
  background-color: #f0f0f0;
  padding: 2px 4px;
  border-radius: 3px;
}

.preview-pane pre {
  background-color: #f0f0f0;
  padding: 10px;
  border-radius: 5px;
  overflow-x: auto; /* 긴 코드 줄 스크롤 */
}

.preview-pane pre code {
  background-color: transparent;
  padding: 0;
}

.preview-pane blockquote {
  border-left: 3px solid #ccc;
  padding-left: 10px;
  color: #666;
  margin-left: 0;
}