현재 이 블로그에서는 next-mdx-remote
의 compileMDX
를 사용하여 MDX 파일을 서버사이드에서 컴파일하고 있다.
그러나 서버사이드에서 컴파일하는 방식은 MDX 내부에서 클라이언트 사이드에서만 동작하는 코드를 사용할 수 없는 문제가 있다.
로딩 중...
<button onClick={() => alert('Hello')}>Click me</button>
compileMDX
의 components
옵션에 커스텀 컴포넌트를 추가할 수 있도록 하는 방법이 있기 때문에, 이를 통해 MDX 파일에
직접 구현한 컴포넌트를 사용할 수 있다.
로딩 중...
커스텀 컴포넌트를 추가하기 위해서는 일반적인 JSX 코드를 작성하는 것과 같은 방식으로 컴포넌트를 작성하면 된다.
'use client';
interface InteractiveButtonProps {
children: React.ReactNode;
onClick?: () => void;
className?: string;
}
export default function InteractiveButton({
children,
onClick = () => alert('Hello from MDX!'),
className = "px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
}: InteractiveButtonProps) {
return (
<button onClick={onClick} className={className}>
{children}
</button>
);
}
그 다음 compileMDX
의 components
옵션에 커스텀 컴포넌트를 추가한다.
const { content, frontmatter } = await compileMDX<FrontMatter>({
source,
options: {
parseFrontmatter: true,
mdxOptions: {
remarkPlugins: [remarkGfm],
rehypePlugins: [
rehypeSlug,
[rehypeAutolinkHeadings, autolinkHeadingsOptions],
[rehypePrettyCode, prettyCodeOptions],
],
},
},
components: {
InteractiveButton,
},
});
MDX 파일에서 커스텀 컴포넌트를 사용하는 방법은 다음과 같다.
...
<InteractiveButton>
Click me
</InteractiveButton>
...
편의를 위해 여러가지 컴포넌트를 구현하여 사용하고 있는데 그 중 몇 가지를 소개한다.
외부 웹 페이지를 미리보기 할 수 있는 컴포넌트이다.
여러 컴포넌트를 하나의 컴포넌트로 묶어서 사용할 수 있는 컴포넌트이다.
탭 기능을 활성화하여 각 컴포넌트를 탭으로 전환할 수도 있다:
<ConnectedComponent enableTab>
...
</ConnectedComponent>
특정 경로의 파일을 읽어와서 소스 코드를 보여주는 컴포넌트이다. 이 컴포넌트는 MDX 파일에 코드를 한번 더 작성해야하는 번거로움을 줄여준다. 또한 파일 수정 시 자동으로 코드가 업데이트되는 편리한 기능을 제공한다.
// 특정 경로 파일의 소스 코드를 보여주는 컴포넌트
import fs from "fs";
import path from "path";
import { compileMDX } from "next-mdx-remote/rsc";
import { rehypePrettyCode } from "rehype-pretty-code";
import CompiledMDXPre from "./CompiledMDXPre";
interface FrontMatter {
title: string;
description: string;
tags: string[];
}
export default async function RawSource({ src }: { src: string }) {
/** @type {import('rehype-pretty-code').Options} */
const prettyCodeOptions = {
keepBackground: false,
theme: "github-dark",
defaultLang: "text",
};
const suffix = src.split(".").pop();
const fileContent = fs.readFileSync(path.join(process.cwd(), src), "utf8");
const { content } = await compileMDX<FrontMatter>({
source: `\`\`\`${suffix}\n${fileContent}\n\`\`\``,
options: {
mdxOptions: {
rehypePlugins: [
[rehypePrettyCode, prettyCodeOptions]
],
},
},
components: {
pre: CompiledMDXPre,
}
});
return content;
}
디렉토리의 구조를 시각화하여 보여주는 컴포넌트이다.
앞서 소개한 공통 컴포넌트 외에 해당 마크다운 내에서만 사용하는 컴포넌트를 작성할 수 있다.
fs
모듈을 사용하여 파일을 읽어오고, 파일 이름을 키로 하여 컴포넌트를 임포트한다.
// 커스텀 컴포넌트 파일 들 동적으로 임포트
// 존재하는 경우만 임포트
const checkCustomComponentDir = fs.existsSync(path.join(process.cwd(), "app/components/mdx", slug));
let customComponents: Record<string, React.ComponentType<any>> = {};
if (checkCustomComponentDir) {
const customComponentList = fs.readdirSync(path.join(process.cwd(), "app/components/mdx", slug));
customComponents = customComponentList.reduce((acc, file) => {
const component = require(`../components/mdx/${slug}/${file}`).default;
acc[file.replace(".tsx", "")] = component;
return acc;
}, {} as Record<string, React.ComponentType<any>>);
}
// MDX 컴파일
const { content, frontmatter } = await compileMDX<FrontMatter>({
source,
options: {
parseFrontmatter: true,
mdxOptions: {
remarkPlugins: [remarkGfm],
rehypePlugins: [
rehypeSlug,
[rehypeAutolinkHeadings, autolinkHeadingsOptions],
[rehypePrettyCode, prettyCodeOptions],
],
},
},
components: {
img: CompiledMDXImage,
pre: CompiledMDXPre,
InteractiveButton,
PreviewWeb,
MDXComponentWrapper,
ConnectedComponent,
ConnectedComponentItem,
MDXToComponent,
FolderStructure,
RawSource,
...customComponents,
},
});
이 포스트의 id를 통해 파일 경로를 동적으로 작성할 수 있다. 현재 포스트의 id는 using-custom-components-in-serverside-mdx
이다.
mdx
디렉토리에 포스트의 id를 포함한 디렉토리를 생성하고, 그 안에 컴포넌트 파일을 생성하면 된다.
export default function HelloWorld() {
return <div className="text-2xl font-bold">Hello World ✨✨✨</div>;
}
MDX 파일에서 HelloWorld
컴포넌트를 사용할 수 있다.
<HelloWorld />