문서
App Router 사용하기
MDX

Markdown 그리고 MDX

Markdown은 텍스트 서식을 지정하는 데 사용되는 경량 마크업 언어입니다. 일반 텍스트 구문을 사용하여 작성하고 구조적으로 유효한 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는 마크다운의 상위 집합으로, 마크다운 파일에서 직접 JSX를 작성할 수 있게 해줍니다. 이는 동적 상호작용을 추가하고 콘텐츠 내에 React 컴포넌트를 포함시키는 강력한 방법입니다.

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

알아두면 좋은 점: 완전한 작동 예제는 Portfolio Starter Kit 템플릿을 참조하세요.

종속성 설치

@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

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 파일이 애플리케이션에서 페이지, 라우트 또는 가져오기로 작동할 수 있습니다.

mdx-components.tsx 파일 추가

전역 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,
  };
}

알아두면 좋은 점:

MDX 렌더링

Next.js의 파일 기반 라우팅을 사용하거나 MDX 파일을 다른 페이지로 가져와서 MDX를 렌더링할 수 있습니다.

파일 기반 라우팅 사용

파일 기반 라우팅을 사용할 때, 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 페이지가 표시되어야 합니다.

import 사용

/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 페이지가 표시되어야 합니다.

사용자 정의 스타일과 컴포넌트 사용하기

마크다운은 렌더링될 때 기본 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 요소에 매핑되는 사용자 정의 컴포넌트를 제공할 수 있습니다. 스타일과 컴포넌트는 전역적으로, 로컬로, 그리고 공유 레이아웃과 함께 구현할 수 있습니다.

전역 스타일과 컴포넌트

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,
  };
}

로컬 스타일과 컴포넌트

가져온 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} />;
}

공유 레이아웃

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>;
}

Tailwind typography 플러그인 사용하기

애플리케이션 스타일링에 Tailwind를 사용하고 있다면, @tailwindcss/typography 플러그인을 사용하여 Tailwind 구성과 스타일을 마크다운 파일에서 재사용할 수 있습니다.

이 플러그인은 마크다운과 같은 소스에서 오는 콘텐츠 블록에 타이포그래피 스타일을 추가하는 데 사용할 수 있는 prose 클래스 세트를 추가합니다.

Tailwind typography를 설치하고 공유 레이아웃과 함께 사용하여 원하는 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>
  );
}

프론트매터

프론트매터는 페이지에 대한 데이터를 저장하는 데 사용할 수 있는 YAML과 유사한 키/값 쌍입니다. @next/mdx는 기본적으로 프론트매터를 지원하지 않지만, MDX 콘텐츠에 프론트매터를 추가하기 위한 다음과 같은 많은 솔루션이 있습니다:

@next/mdx는 다른 JavaScript 컴포넌트와 마찬가지로 내보내기를 사용할 수 있게 합니다:

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 모듈이나 globby와 같은 패키지를 사용하여 포스트 디렉토리를 읽고 메타데이터를 추출할 수 있습니다.

알아두면 좋은 점:

  • fs, globby 등은 서버 사이드에서만 사용할 수 있습니다.
  • 완전한 작동 예제는 Portfolio Starter Kit 템플릿을 참조하세요.

Remark 및 Rehype 플러그인

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

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

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);

원격 MDX

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

알아두면 좋은 점: 주의해서 진행하세요. 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가 표시되어야 합니다.

심층 분석: 마크다운을 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 생태계에는 구문 강조, 제목 링크, 목차 생성 등을 위한 플러그인이 포함되어 있습니다.

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

Rust 기반 MDX 컴파일러 사용하기 (실험적)

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를 처리할 때 필요합니다.

도움이 되는 링크