Markdown and MDX

Markdown (opens in a new tab)은 텍스트를 포맷하는 데 사용되는 경량 마크업 언어입니다. 이를 통해 일반 텍스트 구문을 사용하여 작성하고 구조적으로 유효한 HTML로 변환할 수 있습니다. 웹사이트 및 블로그의 콘텐츠 작성에 자주 사용됩니다.

다음과 같이 작성합니다...

I **love** using [Next.js](https://nextjs.org/)

출력:

<p>I <strong>love</strong> using <a href="https://nextjs.org/">Next.js</a></p>

MDX (opens in a new tab)는 마크다운의 상위 집합으로, JSX (opens in a new tab)를 마크다운 파일 내에서 직접 작성할 수 있게 해줍니다. 이를 통해 동적 인터랙티브 기능을 추가하고 콘텐츠 내에 React 컴포넌트를 삽입할 수 있는 강력한 방법을 제공합니다.

Next.js는 애플리케이션 내의 로컬 MDX 콘텐츠와 서버에서 동적으로 가져오는 원격 MDX 파일을 모두 지원할 수 있습니다. Next.js 플러그인은 마크다운과 React 컴포넌트를 HTML로 변환하는 작업을 처리하며, Server Components(기본 App Router)에서도 사용을 지원합니다.

참고: 완전한 작동 예제를 보려면 Portfolio Starter Kit (opens in a new tab) 템플릿을 참조하세요.

Install dependencies

@next/mdx 패키지와 관련 패키지는 Next.js가 마크다운과 MDX를 처리할 수 있도록 구성하는 데 사용됩니다. 이 패키지는 로컬 파일에서 데이터를 소싱하여 /pages 또는 /app 디렉토리 내에서 .md 또는 .mdx 확장자를 가진 페이지를 생성할 수 있습니다.

다음 패키지를 설치하여 Next.js에서 MDX를 렌더링하세요:

Terminal
npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx

Configure next.config.mjs

프로젝트 루트에 있는 next.config.mjs 파일을 업데이트하여 MDX를 사용하도록 구성합니다:

next.config.mjs
import createMDX from '@next/mdx'
 
/** @type {import('next').NextConfig} */
const nextConfig = {
  // 마크다운 및 MDX 파일을 포함하도록 `pageExtensions`를 구성합니다
  pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
  // 선택적으로 다른 Next.js 구성 추가 가능
}
 
const withMDX = createMDX({
  // 원한다면 여기에서 마크다운 플러그인을 추가하세요
})
 
// MDX 구성과 Next.js 구성을 병합합니다
export default withMDX(nextConfig)

이렇게 하면 .md.mdx 파일이 애플리케이션 내에서 페이지, 라우트 또는 가져오기(import)로 작동할 수 있습니다.

Add an mdx-components.tsx file

글로벌 MDX 컴포넌트를 정의하기 위해 프로젝트 루트에 mdx-components.tsx (또는 .js) 파일을 생성합니다. 예를 들어, pages 또는 app과 같은 수준에 생성하거나, 해당되는 경우 src 폴더 안에 생성합니다.

mdx-components.tsx
import type { MDXComponents } from 'mdx/types'
 
export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    ...components,
  }
}
mdx-components.js
export function useMDXComponents(components) {
  return {
    ...components,
  }
}

참고:

Rendering MDX

파일 기반 라우팅을 사용하여 MDX를 렌더링하거나 다른 페이지에 MDX 파일을 가져올 수 있습니다.

Using file based routing

파일 기반 라우팅을 사용할 때, MDX 페이지를 다른 페이지처럼 사용할 수 있습니다.

App Router 앱에서는 메타데이터를 사용할 수 있습니다.

/app 디렉토리에 새로운 MDX 페이지를 생성합니다:

  my-project
  ├── app
  │   └── mdx-page
  │       └── page.(mdx/md)
  |── mdx-components.(tsx/js)
  └── package.json

이 파일들에서 MDX를 사용할 수 있으며, MDX 페이지 내에서 React 컴포넌트를 직접 가져올 수 있습니다:

import { MyComponent } from 'my-component'
 
# Welcome to my MDX page!
 
This is some **bold** and _italics_ text.
 
This is a list in markdown:
 
- One
- Two
- Three
 
Checkout my React component:
 
<MyComponent />

/mdx-page 경로로 이동하면 렌더링된 MDX 페이지가 표시됩니다.

Using imports

/app 디렉토리에 새로운 페이지를 만들고 원하는 곳에 MDX 파일을 생성합니다:

  my-project
  ├── app
  │   └── mdx-page
  │       └── page.(tsx/js)
  ├── markdown
  │   └── welcome.(mdx/md)
  |── mdx-components.(tsx/js)
  └── package.json

이 파일들에서 MDX를 사용할 수 있으며, MDX 페이지 내에서 React 컴포넌트를 직접 가져올 수 있습니다:

markdown/welcome.mdx
import { MyComponent } from 'my-component'
 
# Welcome to my MDX page!
 
This is some **bold** and _italics_ text.
 
This is a list in markdown:
 
- One
- Two
- Three
 
Checkout my React component:
 
<MyComponent />

페이지 안에 MDX 파일을 가져와서 콘텐츠를 표시합니다:

app/mdx-page/page.tsx
import Welcome from '@/markdown/welcome.mdx'
 
export default function Page() {
  return <Welcome />
}
app/mdx-page/page.js
import Welcome from '@/markdown/welcome.mdx'
 
export default function Page() {
  return <Welcome />
}

/mdx-page 경로로 이동하면 렌더링된 MDX 페이지가 표시됩니다.

Using custom styles and components

마크다운은 렌더링될 때 네이티브 HTML 요소로 매핑됩니다. 예를 들어, 다음 마크다운을 작성하면:

## This is a heading
 
This is a list in markdown:
 
- One
- Two
- Three

다음 HTML이 생성됩니다:

<h2>This is a heading</h2>
 
<p>This is a list in markdown:</p>
 
<ul>
  <li>One</li>
  <li>Two</li>
  <li>Three</li>
</ul>

마크다운을 스타일링하려면 생성된 HTML 요소에

매핑되는 사용자 정의 컴포넌트를 제공할 수 있습니다. 스타일과 컴포넌트는 전역, 로컬, 공유 레이아웃으로 구현할 수 있습니다.

Global styles and components

mdx-components.tsx에 스타일과 컴포넌트를 추가하면 애플리케이션의 모든 MDX 파일에 영향을 미칩니다.

mdx-components.tsx
import type { MDXComponents } from 'mdx/types'
import Image, { ImageProps } from 'next/image'
 
// 이 파일을 사용하여 MDX 파일에서 사용할 사용자 정의 React 컴포넌트를 제공할 수 있습니다.
// 원하는 모든 React 컴포넌트를 가져와 사용할 수 있으며, 인라인 스타일, 다른 라이브러리의 컴포넌트 등을 포함할 수 있습니다.
 
export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    // 스타일을 추가하기 위해 내장된 컴포넌트를 사용자 정의할 수 있습니다.
    h1: ({ children }) => (
      <h1 style={{ color: 'red', fontSize: '48px' }}>{children}</h1>
    ),
    img: (props) => (
      <Image
        sizes="100vw"
        style={{ width: '100%', height: 'auto' }}
        {...(props as ImageProps)}
      />
    ),
    ...components,
  }
}
mdx-components.js
import Image from 'next/image'
 
// 이 파일을 사용하여 MDX 파일에서 사용할 사용자 정의 React 컴포넌트를 제공할 수 있습니다.
// 원하는 모든 React 컴포넌트를 가져와 사용할 수 있으며, 인라인 스타일, 다른 라이브러리의 컴포넌트 등을 포함할 수 있습니다.
 
export function useMDXComponents(components) {
  return {
    // 스타일을 추가하기 위해 내장된 컴포넌트를 사용자 정의할 수 있습니다.
    h1: ({ children }) => (
      <h1 style={{ color: 'red', fontSize: '48px' }}>{children}</h1>
    ),
    img: (props) => (
      <Image
        sizes="100vw"
        style={{ width: '100%', height: 'auto' }}
        {...props}
      />
    ),
    ...components,
  }
}

Local styles and components

특정 페이지에 로컬 스타일과 컴포넌트를 적용하려면, 가져온 MDX 컴포넌트에 이를 전달하여 사용할 수 있습니다. 이는 전역 스타일 및 컴포넌트와 병합되며 이를 재정의합니다.

app/mdx-page/page.tsx
import Welcome from '@/markdown/welcome.mdx'
 
function CustomH1({ children }) {
  return <h1 style={{ color: 'blue', fontSize: '100px' }}>{children}</h1>
}
 
const overrideComponents = {
  h1: CustomH1,
}
 
export default function Page() {
  return <Welcome components={overrideComponents} />
}
app/mdx-page/page.js
import Welcome from '@/markdown/welcome.mdx'
 
function CustomH1({ children }) {
  return <h1 style={{ color: 'blue', fontSize: '100px' }}>{children}</h1>
}
 
const overrideComponents = {
  h1: CustomH1,
}
 
export default function Page() {
  return <Welcome components={overrideComponents} />
}

Shared layouts

MDX 페이지 간에 레이아웃을 공유하려면 App Router와 함께 내장된 레이아웃 지원을 사용할 수 있습니다.

app/mdx-page/layout.tsx
export default function MdxLayout({ children }: { children: React.ReactNode }) {
  // 여기에서 모든 공유 레이아웃 또는 스타일을 생성하세요
  return <div style={{ color: 'blue' }}>{children}</div>
}
app/mdx-page/layout.js
export default function MdxLayout({ children }) {
  // 여기에서 모든 공유 레이아웃 또는 스타일을 생성하세요
  return <div style={{ color: 'blue' }}>{children}</div>
}

Using Tailwind typography plugin

애플리케이션 스타일링에 Tailwind (opens in a new tab)를 사용하는 경우, @tailwindcss/typography 플러그인 (opens in a new tab)을 사용하면 Tailwind 구성 및 스타일을 마크다운 파일에서 재사용할 수 있습니다.

이 플러그인은 마크다운과 같은 소스에서 가져온 콘텐츠 블록에 타이포그래픽 스타일을 추가하는 prose 클래스를 제공합니다.

Tailwind typography 설치 (opens in a new tab)공유 레이아웃 사용하여 원하는 prose를 추가하세요.

app/mdx-page/layout.tsx
export default function MdxLayout({ children }: { children: React.ReactNode }) {
  // 여기에서 모든 공유 레이아웃 또는 스타일을 생성하세요
  return (
    <div className="prose prose-headings:mt-8 prose-headings:font-semibold prose-headings:text-black prose-h1:text-5xl prose-h2:text-4xl prose-h3:text-3xl prose-h4:text-2xl prose-h5:text-xl prose-h6:text-lg dark:prose-headings:text-white">
      {children}
    </div>
  )
}
app/mdx-page/layout.js
export default function MdxLayout({ children }) {
  // 여기에서 모든 공유 레이아웃 또는 스타일을 생성하세요
  return (
    <div className="prose prose-headings:mt-8 prose-headings:font-semibold prose-headings:text-black prose-h1:text-5xl prose-h2:text-4xl prose-h3:text-3xl prose-h4:text-2xl prose-h5:text-xl prose-h6:text-lg dark:prose-headings:text-white">
      {children}
    </div>
  )
}

Frontmatter

Frontmatter는 페이지에 대한 데이터를 저장하는 데 사용되는 YAML과 같은 키/값 쌍입니다. @next/mdx는 기본적으로 frontmatter를 지원하지 않지만, MDX 콘텐츠에 frontmatter를 추가하는 다양한 솔루션이 있습니다:

@next/mdx는 다른 JavaScript 컴포넌트처럼 export를 사용할 수 있도록 해줍니다:

content/blog-post.mdx
export const metadata = {
  author: 'John Doe',
}
 
# Blog post

이제 MDX 파일 외부에서 메타데이터를 참조할 수 있습니다:

app/blog/page.tsx
import BlogPost, { metadata } from '@/content/blog-post.mdx'
 
export default function Page() {
  console.log('metadata': metadata)
  //=> { author: 'John Doe' }
  return <BlogPost />
}
app/blog/page.js
import BlogPost, { metadata } from '@/content/blog-post.mdx'
 
export default function Page() {
  console.log('metadata': metadata)
  //=> { author: 'John Doe' }
  return <BlogPost />
}

이의 일반적인 사용 사례는 MDX 컬렉션을 반복하고 데이터를 추출하려는 경우입니다. 예를 들어, 모든 블로그 게시물에서 블로그 인덱스 페이지를 생성하는 것입니다. Node의 fs 모듈 (opens in a new tab)이나 globby (opens in a new tab)와 같은 패키지를 사용하여 게시물 디렉토리를 읽고 메타데이터를 추출할 수 있습니다.

참고:

Remark and Rehype Plugins

선택적으로 remarkrehype 플러그인을 제공하여 MDX 콘텐츠를 변환할 수 있습니다.

예를 들어, remark-gfm을 사용하여 GitHub Flavored Markdown을 지원할 수 있습니다.

remarkrehype 생태계는 ESM 전용이므로 구성 파일로 next.config.mjs를 사용해야 합니다.

next.config.mjs
import remarkGfm from 'remark-gfm'
import createMDX from '@next/mdx'
 
/** @type {import('next').NextConfig} */
const nextConfig = {
  // MDX 파일을 포함하도록 `pageExtensions`를 구성합니다
  pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
  // 선택적으로 다른 Next.js 구성 추가 가능
}
 
const withMDX = createMDX({
  // 원한다면 여기에서 마크다운 플러그인을 추가하세요
  options: {
    remarkPlugins: [remarkGfm],
    rehypePlugins: [],
  },
})
 
// MDX 구성과 Next.js 구성을 병합합니다
export default withMDX(nextConfig)

Remote MDX

MDX 파일이나 콘텐츠가 다른 곳에 있는 경우, 서버에서 동적으로 가져올 수 있습니다. 이는 별도의 로컬 폴더, CMS, 데이터베이스 또는 다른 곳에 저장된 콘텐츠에 유용합니다. 이 용도로 인기 있는 커뮤니티 패키지는 next-mdx-remote (opens in a new tab)입니다.

참고: 신중하게 사용하세요. MDX는 JavaScript로 컴파일되며 서버에서 실행됩니다. 신뢰할 수 있는 소스에서만 MDX 콘텐츠를 가져와야 하며, 그렇지 않으면 원격 코드 실행(RCE)로 이어질 수 있습니다.

다음 예제는 next-mdx-remote를 사용합니다:

app/mdx-page-remote/page.tsx
import { MDXRemote } from 'next-mdx-remote/rsc'
 
export default async function RemoteMdxPage() {
  // MDX 텍스트 - 로컬 파일, 데이터베이스, CMS, fetch 등에서 가져올 수 있습니다...
  const res = await fetch('https://...')
  const markdown = await res.text()
  return <MDXRemote source={markdown} />
}
app/mdx-page-remote/page.js
import { MDXRemote } from 'next-mdx-remote/rsc'
 
export default async function RemoteMdxPage() {
  // MDX 텍스트 - 로컬 파일, 데이터베이스, CMS, fetch 등에서 가져올 수 있습니다...
  const res = await fetch('https://...')
  const markdown = await res.text()
  return <MDXRemote source={markdown} />
}

/mdx-page-remote 경로로 이동하면 렌더링된 MDX가 표시됩니다.

Deep Dive: How do you transform markdown into HTML?

React는 본래 마크다운을 이해하지 못합니다. 마크다운 평문은 먼저 HTML로 변환되어야 합니다. 이는 remarkrehype를 사용하여 수행할 수 있습니다.

remark는 마크다운을 중심으로 한 도구 생태계입니다. rehype는 HTML을 중심으로 한 생태계입니다. 예를 들어, 다음 코드 스니펫은 마크다운을 HTML로 변환합니다:

import { unified } from 'unified'
import remarkParse from 'remark-parse'
import remarkRehype from 'remark-rehype'
import rehypeSanitize from 'rehype-sanitize'
import rehypeStringify from 'rehype-stringify'
 
main()
 
async function main() {
  const file = await unified()
    .use(remarkParse) // 마크다운 AST로 변환
    .use(remarkRehype) // HTML AST로 변환
    .use(rehypeSanitize) // HTML 입력을 소독
    .use(rehypeStringify) // AST를 직렬화된 HTML로 변환
    .process('Hello, Next.js!')
 
  console.log(String(file)) // <p>Hello, Next.js!</p>
}

remarkrehype 생태계는 문법 강조 (opens in a new tab), 제목 링크 추가 (opens in a new tab), 목차 생성 (opens in a new tab) 등 다양한 플러그인을 포함합니다.

위에서 설명한 것처럼 @next/mdx를 사용할 때는 직접 remarkrehype를 사용할 필요가 없습니다. 이는 내부적으로 처리됩니다. 여기서는 @next/mdx 패키지가 내부적으로 수행하는 작업에 대한 깊은 이해를 위해 설명하고 있습니다.

Using the Rust-based MDX compiler (experimental)

Next.js는 Rust로 작성된 새로운 MDX 컴파일러를 지원합니다. 이 컴파일러는 아직 실험적이며, 프로덕션 사용을 권장하지 않습니다. 새로운 컴파일러를 사용하려면, withMDX에 전달할 때 next.config.js를 구성해야 합니다:

next.config.js
module.exports = withMDX({
  experimental: {
    mdxRs: true,
  },
})

mdxRs는 mdx 파일을 변환하는 방법을 구성하기 위해 객체도 허용합니다.

next.config.js
module.exports = withMDX({
  experimental: {
    mdxRs: {
      jsxRuntime?: string            // 커스텀 jsx 런타임
      jsxImportSource?: string       // 커스텀 jsx import 소스,
      mdxType?: 'gfm' | 'commonmark' // 구문 분석 및 변환에 사용할 mdx 구문 종류 구성
    },
  },
})

참고:

이 옵션은 Turbopack(next dev --turbo)을 사용하여 마크다운 및 MDX를 처리할 때 필요합니다.

Helpful Links