MDX — Next.js에서 마크다운으로 콘텐츠 작성하기

Next.js App Router에서 @next/mdx와 next-mdx-remote를 사용해 MDX 콘텐츠를 렌더링하는 방법을 설명합니다. 커스텀 컴포넌트, frontmatter, remark/rehype 플러그인 활용까지 다룹니다.

· 5 min read · PALDYN Team

지난 글에서 다국어 지원을 구현하는 방법을 살펴봤다. 이번 글은 MDX다. MDX는 마크다운에 JSX를 섞을 수 있는 포맷으로, 블로그·문서 사이트에 최적이다. Next.js는 @next/mdxnext-mdx-remote 두 가지 방식을 지원한다.

MDX란

---
title: 나의 첫 MDX 포스트
---

# 제목

마크다운 문법으로 텍스트를 작성하고...

import { Chart } from './Chart'

**JSX 컴포넌트도 바로 사용**할 수 있습니다.

<Chart data={[10, 20, 30]} />

마크다운의 간결함과 React 컴포넌트의 표현력을 함께 쓸 수 있다.

MDX 처리 파이프라인

방식 1: @next/mdx (파일 기반)

프로젝트 내 .mdx 파일을 페이지처럼 사용하는 방식이다. 소규모 블로그나 문서 사이트에 적합하다.

npm install @next/mdx @mdx-js/loader @mdx-js/react
npm install -D @types/mdx
// next.config.ts
import type { NextConfig } from 'next'
import createMDX from '@next/mdx'

const withMDX = createMDX({
  options: {
    remarkPlugins: [],
    rehypePlugins: [],
  },
})

const nextConfig: NextConfig = {
  pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
}

export default withMDX(nextConfig)

pageExtensionsmdmdx를 추가하면 app/blog/post.mdx/blog/post 경로로 자동 라우팅된다.

// mdx-components.tsx (프로젝트 루트 — 필수)
import type { MDXComponents } from 'mdx/types'

export function useMDXComponents(components: MDXComponents): MDXComponents {
  return {
    h1: ({ children }) => (
      <h1 className="text-3xl font-bold my-6 text-gray-100">{children}</h1>
    ),
    h2: ({ children }) => (
      <h2 className="text-2xl font-semibold my-5 text-gray-200">{children}</h2>
    ),
    code: ({ children }) => (
      <code className="bg-gray-800 px-1.5 py-0.5 rounded text-sm font-mono">
        {children}
      </code>
    ),
    ...components,
  }
}

MDX 커스텀 컴포넌트

방식 2: next-mdx-remote (동적 콘텐츠)

CMS, 데이터베이스, 파일 시스템 등 외부 소스에서 MDX 문자열을 불러와 렌더링하는 방식이다.

npm install next-mdx-remote
// app/blog/[slug]/page.tsx
import { MDXRemote } from 'next-mdx-remote/rsc'
import { readFileSync } from 'fs'
import path from 'path'

// 커스텀 컴포넌트
const components = {
  Callout: ({ children }: { children: React.ReactNode }) => (
    <div className="border-l-4 border-blue-500 pl-4 py-2 bg-blue-950">
      {children}
    </div>
  ),
  Chart: dynamic(() => import('@/components/Chart'), { ssr: false }),
}

export default async function BlogPost({
  params: { slug },
}: {
  params: { slug: string }
}) {
  const filePath = path.join(process.cwd(), 'content', `${slug}.mdx`)
  const source = readFileSync(filePath, 'utf-8')

  return (
    <article className="prose prose-invert max-w-none">
      <MDXRemote
        source={source}
        components={components}
        options={{
          parseFrontmatter: true,
        }}
      />
    </article>
  )
}

/rsc 경로로 가져오면 Server Component에서 직접 사용 가능하다. 별도 hydration 없이 서버에서 렌더링된다.

frontmatter 처리

---
title: "MDX로 작성한 블로그 포스트"
pubDate: "2026-06-06"
tags: ["nextjs", "mdx"]
---

본문 내용...
// app/blog/[slug]/page.tsx
import { compileMDX } from 'next-mdx-remote/rsc'

interface Frontmatter {
  title: string
  pubDate: string
  tags: string[]
}

export default async function BlogPost({
  params: { slug },
}: {
  params: { slug: string }
}) {
  const source = await fetchPostSource(slug) // MDX 문자열 가져오기

  const { content, frontmatter } = await compileMDX<Frontmatter>({
    source,
    options: { parseFrontmatter: true },
  })

  return (
    <article>
      <h1>{frontmatter.title}</h1>
      <time>{frontmatter.pubDate}</time>
      <div className="prose prose-invert">{content}</div>
    </article>
  )
}

remark/rehype 플러그인

npm install rehype-highlight remark-gfm rehype-slug rehype-autolink-headings
// next.config.ts (또는 compileMDX options)
import rehypeHighlight from 'rehype-highlight'
import remarkGfm from 'remark-gfm'
import rehypeSlug from 'rehype-slug'

const withMDX = createMDX({
  options: {
    remarkPlugins: [
      remarkGfm, // GitHub Flavored Markdown (표, 체크박스 등)
    ],
    rehypePlugins: [
      rehypeSlug,          // 헤딩에 id 자동 추가
      rehypeHighlight,     // 코드 블록 신택스 하이라이팅
    ],
  },
})

자주 쓰이는 플러그인:

플러그인기능
remark-gfm표, 취소선, 체크박스
rehype-slug헤딩에 id 속성 자동 추가
rehype-autolink-headings헤딩 앵커 링크
rehype-highlight코드 신택스 하이라이팅
rehype-pretty-codeShiki 기반 고품질 하이라이팅

목차(TOC) 자동 생성

// lib/mdx.ts
import { compileMDX } from 'next-mdx-remote/rsc'
import rehypeSlug from 'rehype-slug'

interface TocItem {
  id: string
  title: string
  level: number
}

export async function getMdxContent(source: string) {
  const toc: TocItem[] = []

  const { content, frontmatter } = await compileMDX({
    source,
    options: {
      parseFrontmatter: true,
      mdxOptions: {
        rehypePlugins: [
          rehypeSlug,
          // 커스텀 플러그인으로 TOC 수집
        ],
      },
    },
  })

  return { content, frontmatter, toc }
}

지난 글: 국제화(i18n) — Next.js에서 다국어 지원 구현하기

다음 글: 에러 모니터링 — Sentry로 프로덕션 오류 추적하기


읽어주셔서 감사합니다. 😊