Logo

Markdown을 HTML로 변환 (unified, remark, rehype)

마크다운(Markdown)은 경량화된 마크업 언어로 구조적인 텍스트의 편집 용도로 여러 가지 도구에서 사용되고 있습니다. 대표적인 사례인 Github의 경우, 확장된 형태의 마크다운을 지원하고 있기 때문에 이슈(issue)나 PR(pull reqeust)를 생성할 때 쉽게 접해볼 수 있습니다.

이번 포스팅에서는 Markdown 형태의 텍스트를 HTML 형태의 택스트로 변환하는 방법에 대해서 살펴보겠습니다.

Unified, Remark, Rehype

remark는 마크다운을 처리할 때 사용하는 라이브러리이고, rehype은 HTML을 처리할 때 사용하는 라이브러리입니다. 이 두 개의 라이브러리는 다양한 형식의 텍스트의 범용 처리를 지원하고 있는 unified라는 상위 프로젝트에 소속되어 있습니다.

unified 라이브러리를 사용하면 Markdown이든 HTML이든 형식에 구애받지 않고 동일한 API(applicaiton programming interface)를 통해서 텍스트를 처리할 수 있다는 큰 이점이 있습니다. unified 라이브러리의 API는 use() 함수를 연쇄적으로 호출하여 원하는 작업을 순차적으로 처리하도록 설계되어 있습니다. 내부적으로 unist(universal syntax tree)라는 추상 구문 트리(abstract syntax tree, AST)를 이용하여 매우 효율적으로 텍스트를 처리해주기 때문에 Prettier, ESLint 등 많은 프로젝트에서 사용되고 있습니다.

패키지 설치

unified는 다양한 하위 라이브러리로 이루어져있기 때문에 필요에 따라 여러 패키지를 설치하여 조합해서 사용합니다.

$ npm i unified remark-parse remark-rehype rehype-stringify

Markdown 구문 트리로 변환

먼저 텍스트 형태의 Markdown을 구문 트리(abstract tree)로 바꿔보도록 하겠습니다.

import unified from "unified";
import markdown from "remark-parse";

const mdText = `
# Our Project

Hello, **Markdown!**.
`;

const mdAst = unified().use(markdown).parse(mdText);

console.log(mdAst);

아래와 같이 Markdown이 root를 최상위 노드로 갖는 계층적인 트리의 형태로 변환이 되었습니다. 모든 노드는 type 속성을 가지고 있어서 어떤 데이터를 표현하고 있는지 나타내고 있으며, position 속성을 통해 어디서 시작하고 어디서 끝나는지를 파악할 수 있습니다.

{
  "type": "root",
  "children": [
    {
      "type": "heading",
      "depth": 1,
      "children": [
        {
          "type": "text",
          "value": "Our Project",
          "position": {
            "start": {
              "line": 2,
              "column": 3,
              "offset": 3
            },
            "end": {
              "line": 2,
              "column": 14,
              "offset": 14
            },
            "indent": []
          }
        }
      ],
      "position": {
        "start": {
          "line": 2,
          "column": 1,
          "offset": 1
        },
        "end": {
          "line": 2,
          "column": 14,
          "offset": 14
        },
        "indent": []
      }
    },
    {
      "type": "paragraph",
      "children": [
        {
          "type": "text",
          "value": "Hello, ",
          "position": {
            "start": {
              "line": 4,
              "column": 1,
              "offset": 16
            },
            "end": {
              "line": 4,
              "column": 8,
              "offset": 23
            },
            "indent": []
          }
        },
        {
          "type": "strong",
          "children": [
            {
              "type": "text",
              "value": "Markdown",
              "position": {
                "start": {
                  "line": 4,
                  "column": 10,
                  "offset": 25
                },
                "end": {
                  "line": 4,
                  "column": 18,
                  "offset": 33
                },
                "indent": []
              }
            }
          ],
          "position": {
            "start": {
              "line": 4,
              "column": 8,
              "offset": 23
            },
            "end": {
              "line": 4,
              "column": 20,
              "offset": 35
            },
            "indent": []
          }
        },
        {
          "type": "text",
          "value": "!",
          "position": {
            "start": {
              "line": 4,
              "column": 20,
              "offset": 35
            },
            "end": {
              "line": 4,
              "column": 21,
              "offset": 36
            },
            "indent": []
          }
        }
      ],
      "position": {
        "start": {
          "line": 4,
          "column": 1,
          "offset": 16
        },
        "end": {
          "line": 4,
          "column": 21,
          "offset": 36
        },
        "indent": []
      }
    }
  ],
  "position": {
    "start": {
      "line": 1,
      "column": 1,
      "offset": 0
    },
    "end": {
      "line": 5,
      "column": 1,
      "offset": 37
    }
  }
}

HTML 텍스트로 변환

다음으로 이 Markdown 구문 트리를 HTML 텍스트로 변환해보겠습니다. remark-rehype 패키지를 이용해서 Markdown을 HTML로 변환 후, rehype-stringify 패키지를 이용해서 다시 텍스트 형태로 변환하겠습니다.

import unified from "unified";
import markdown from "remark-parse";
import remark2rehype from "remark-rehype";
import html from "rehype-stringify";

const mdText = `
# Our Project

Hello, **Markdown**!
`;

const html_text = unified()
  .use(markdown)
  .use(remark2rehype)
  .use(html)
  .processSync(mdText);

console.log(html_text.toString());```

짜잔! 🎉 최종 변환된 텍스트를 출력해보면 다음과 같이 HTML을 얻을 수 있습니다.

```html
<h1>Our Project</h1>
<p>Hello, <strong>Markdown</strong>!</p>

여기서 그치지 않고, rehype-format 패키지를 이용하여 HTML을 포맷팅하거나, rehype-document 패키지를 이용하여 <html/>, <head/>, <body/> 엘리먼트로 이루어진 완전한 HTML 문서로 변환할 수도 있습니다.

전체 코드

본 포스팅에서 작성한 코드는 아래에서 확인하실 수 있습니다.

마치면서

이상으로 unified, remark, rehype 라이브러리를 이용하여 Markdown 텍스트를 HTML 텍스트로 변환해보았습니다. unified 프로젝트는 Markdown, HTML 뿐만 아니라 다양한 형태의 텍스트 처리를 지원하고 있으니 관심이 있으신 분들은 아래 링크를 통해 다른 라이브러리들도 탐험해보시길 바라겠습니다.