Next.js에서 MDX를 사용하기 위한 여정

2022. 2. 11

MDX를 사용하게 된 이유

사이드 프로젝트로 블로그를 만들기 위해 스펙을 정하던 도중 MDX라는 컴포넌트에 친화적인 마크다운이 있다는 것을 알아냈다.

컴포넌트에 친화적이란 말은 예상이 되겠지만 아래처럼 마크다운에 컴포넌트를 사용할 수 있다는 것이다.

import { Box, Heading } from 'rebass'

MDX using imported components!

<Box>
  <Heading>Here’s a heading</Heading>
</Box>

마침 Next.js를 이용하여 구현할 예정이었기 때문에 이번 기회에 한 번 사용해보기로 하고 본격적인 조사에 들어갔다.

전체 코드를 참고하려면 Blog Repository에 코드들이 있다.

MDX-Bundler

MDX의 공식문서와 여러가지 아티클들을 찾아보던 중 리액트 테스팅 툴을 만드신 켄트아저씨가 만든 MDX-Bundler 라이브러리가 눈에 들어왔다.

esbuild를 사용하여 더욱 빠른 빌드와 Next.js와의 호환성, 그리고 지속적인 이슈 관리와 빵빵한 스폰서들이 있어 지속적으로 사용하기 굉장히 좋아보였다. 또한, 문서화가 친절하게 되어있어 문서를 보면서 금방 구현할 수 있을거라 생각했다.

설치

npm install --save mdx-bundler esbuild

mdx-bundler는 esbuild를 통해서 빌드하기 때문에 함께 설치하지 않으면 추후 빌드시에 문제가 생긴다. (실수로 설치하지 않았다가 하루종일 삽질했던 경험이 있다.. 😅)

사용

mdx-bundler에서 제공되는 몇가지 함수들이 있다. 그 중에 bundleMDX, getMDXComponent를 활용하여 구현해보았다.

bundleMDX

bundleMDX는 특정 mdx 파일을 받아와 mdx를 화면에 그려줄 수 있는 데이터를 제공해주는 비동기 함수다.

객체를 인자로 가지고 가는데 대표적으로 sourcecwd 두가지다.

source는 mdx 파일을 utf-8 형태로 인코딩하여 넘겨주면 mdx에 작성한 내용이 추후 matter 형태로 반환되는데 이를 통해 화면에 mdx를 그려줄 수 있게된다.

cwd작업 디렉토리를 명시하는데 여기서 디렉토리를 명시하면 esbuild를 통해 import가 가능해진다.

즉, mdx에서 컴포넌트를 사용하기 위해서는 import를 해주어야하기 때문에 디렉토리를 명시하여 빌드를 해주어야한다.

나의 경우엔 무난하게 루트 디렉토리를 명시해주었다.

그렇게 두가지를 넘기면 아래와 같다.

const source = fs.readFileSync(path.join(`${POST_PATH}.mdx`), { encoding: 'utf-8' })

const {
  matter: { data, content },
  code,
} = await bundleMDX({
  source,
  cwd: process.cwd(),
})

이렇게 두 값을 넘기면 두 가지 값을 받을 수 있을 수 있는데 mattercode다.

이를 사용하기 위해서는 mdx의 형태를 맞추어주어야 하는데 형태는 아래와 같다.

---
title: Next.js에서 MDX를 사용하기 위한 여정 #required
tags:
  - next.js
  - typescript
  - mdx
published: true # required
date: '2022. 2. 11' # required
description: '완성하자마자 귀신같이 MDX 2가 나왔네? 😠' # required
thumbnailImg: '/thumbnail/2022/02/nextjs/nextjs-mdx.png' # required
---

## MDX를 사용하게 된 이유

사이드 프로젝트로 블로그를 만들기 위해 스펙을 정하던 도중 [MDX](https://mdxjs.com/)라는 **컴포넌트**에 친화적인 마크다운이 있다는 것을 알아냈다.

컴포넌트에 친화적이란 말은 예상이 되겠지만 아래처럼 **마크다운**에 컴포넌트를 사용할 수 있다는 것이다.

...

보면 mdx에 대한 정보를 나타내는 부분과 본문쪽으로 나뉘고 있다.

이 전체적인 부분은 matter라고 하며 여기서 data는 정보에 대한 데이터를 JSON으로, content는 본문 쪽이 문자열로 출력된다.

data

{
  "title": "Next.js에서 MDX를 사용하기 위한 여정",
  "tags": ["next.js", "typescript", "mdx"],
  "published": true,
  "date": "2022. 2. 11",
  "description": "완성하자마자 귀신같이 MDX 2가 나왔네? 😠",
  "thumbnailImg": "/thumbnail/2022/02/nextjs/nextjs-mdx.png"
}

content

## MDX를 사용하게 된 이유

사이드 프로젝트로 블로그를 만들기 위해 스펙을 정하던 도중 [MDX](https://mdxjs.com/)라는 **컴포넌트**에 친화적인 마크다운이 있다는 것을 알아냈다.

컴포넌트에 친화적이란 말은 예상이 되겠지만 아래처럼 **마크다운**에 컴포넌트를 사용할 수 있다는 것이다.

여기서 넘어오는 문자열은 mdx에 작성한 그 자체이기 때문에 추가 라이브러리를 사용해서 다시 가공하지 않는이상 사용하기 힘들었다.

그래서 함수에서 제공하는 다른 값인 code를 활용하는 방법을 선택했다.

getMDXComponent

이 함수는 방금 넘겨받은 code를 인자로 받아 mdx의 본문을 자체 컴포넌트화 시켜서 반환해준다.

components라는 props를 받는데 이는 마크다운에 사용되는 여러가지 문법에 대한 커스텀한 컴포넌트들을 선언하여 주어 그 컴포넌트를 실제 반영하여 그려주기 때문에 굉장히 편리하다.

즉, 위에 content를 사용하지 않고 code를 통해 한 번에 해결이 가능한 것이다.

import { getMDXComponent } from 'mdx-bundler/client'

import MDXComponents from '#components/shared/MDX'

function PostDetail({ post, code }: { post: Post, code: string }) {
  const { title, date, description, tags, thumbnailImg } = post.frontMatter
  const Component = React.useMemo(() => getMDXComponent(code), [code])

  return (
    <>
      <PostSEO
        seo={{
          title,
          description,
          thumbnailImg,
          tags,
        }}
      />
      <section className={styles.container}>
        <h1 className={styles.title}>{title}</h1>
        <span className={styles.date}>{date}</span>
        <Component components={MDXComponents} />
      </section>
    </>
  )
}

export default PostDetail

코드를 보면 code를 받아 바로 컴포넌트로 그려주고 있다. 저렇게 그려주면 아래와 같이 기본적인 스타일을 가진 본문을 뱉어낼 것이다.

content

여기서 추가적으로 스타일링이 필요하다면 mdx에서 제공하는 공통 컴포넌트를 만들어 props로 전달하면 된다.

공통 컴포넌트 구현

공통 컴포넌트에는 여러 종류가 있지만 대표적으로 Hn, p, span, img, ol 등의 태그들이 있다.

저 태그들에 해당되는 컴포넌트를 만들어 객체형태로 제공하면 전달 받은 props의 스타일대로 자동으로 그려준다.

import Code from './Code'
import Paragraph from './Paragraph'
// ...

export default {
  code: Code,
  p: Paragraph,
  // ...
}

블로그에서 사용될 여러가지의 태그들을 만들었고 이를 객체형태로 내보내서 사용하였다.

대표적으로 하나의 컴포넌트를 예를들자면 아래와 같다.

import React from 'react'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism'

import { codeText, syntaxHighlighter } from './styles.css'

const Code = ({
  className,
  children,
}: React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement>) => {
  if (!className) {
    return <code className={codeText}>{children}</code>
  }

  return (
    <SyntaxHighlighter language="javascript" style={vscDarkPlus} customStyle={syntaxHighlighter}>
      {children}
    </SyntaxHighlighter>
  )
}

export default Code

코드에 스타일을 주기 위해서 필요한 라이브러리를 사용하였고 code 태그에 맞는 형태로 반환하면 완성된다.

이렇게 여러가지의 컴포넌트들을 만들고 다시 확인하면

mdx component

위와 같이 스타일이 적용된 컴포넌트를 만날 수가 있다!

지금까지 글을 작성하면서 생각보다 mdx에서 컴포넌트의 활용도는 적었지만, 자바스크립트를 활용할 수 있다는 점에서 굉장히 신선했고 이전에는 여러가지의 라이브러리를 사용하면서 불편하게 만들어야했다면 mdx-bundler를 통해서 간편하게 구현할 수 있다는 점에서 편하고 좋았던 것 같다. 👍👍