돌아가기
#Next.js
#MDX
#React

서버사이드 MDX에서 커스텀 컴포넌트 사용하기

현재 이 블로그에서는 next-mdx-remotecompileMDX를 사용하여 MDX 파일을 서버사이드에서 컴파일하고 있다.

그러나 서버사이드에서 컴파일하는 방식은 MDX 내부에서 클라이언트 사이드에서만 동작하는 코드를 사용할 수 없는 문제가 있다.

onClick 이벤트를 사용하는 컴포넌트를 호출하면 에러가 발생합니다

로딩 중...example

tsx
<button onClick={() => alert('Hello')}>Click me</button>

compileMDXcomponents 옵션에 커스텀 컴포넌트를 추가할 수 있도록 하는 방법이 있기 때문에, 이를 통해 MDX 파일에 직접 구현한 컴포넌트를 사용할 수 있다.

로딩 중...components

커스텀 컴포넌트 사용하기

커스텀 컴포넌트를 추가하기 위해서는 일반적인 JSX 코드를 작성하는 것과 같은 방식으로 컴포넌트를 작성하면 된다.

코드 보기
tsx
'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>
  );
} 

그 다음 compileMDXcomponents 옵션에 커스텀 컴포넌트를 추가한다.

tsx
const { content, frontmatter } = await compileMDX<FrontMatter>({
    source,
    options: {
    parseFrontmatter: true,
    mdxOptions: {
        remarkPlugins: [remarkGfm],
        rehypePlugins: [
        rehypeSlug,
        [rehypeAutolinkHeadings, autolinkHeadingsOptions],
        [rehypePrettyCode, prettyCodeOptions],
        ],
    },
    },
    components: {
      InteractiveButton,
    },
});

MDX 파일에서 커스텀 컴포넌트를 사용하는 방법은 다음과 같다.

mdx
...
<InteractiveButton>
Click me
</InteractiveButton>
...

직접 구현한 컴포넌트

편의를 위해 여러가지 컴포넌트를 구현하여 사용하고 있는데 그 중 몇 가지를 소개한다.

PreviewWeb

외부 웹 페이지를 미리보기 할 수 있는 컴포넌트이다.

웹 미리보기
로딩 중...

ConnectedComponent

여러 컴포넌트를 하나의 컴포넌트로 묶어서 사용할 수 있는 컴포넌트이다.

첫 번째 컴포넌트
두 번째 컴포넌트
세 번째 컴포넌트

탭 기능을 활성화하여 각 컴포넌트를 탭으로 전환할 수도 있다:

mdx
<ConnectedComponent enableTab>
...
</ConnectedComponent>

첫 번째 컴포넌트

RawSource

특정 경로의 파일을 읽어와서 소스 코드를 보여주는 컴포넌트이다. 이 컴포넌트는 MDX 파일에 코드를 한번 더 작성해야하는 번거로움을 줄여준다. 또한 파일 수정 시 자동으로 코드가 업데이트되는 편리한 기능을 제공한다.

tsx
// 특정 경로 파일의 소스 코드를 보여주는 컴포넌트
 
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;
}
 

FolderStructure

디렉토리의 구조를 시각화하여 보여주는 컴포넌트이다.

디렉토리1/
파일1
디렉토리2/
내부 디렉토리1/이렇게 하이라이트도 할 수 있다
파일2
파일3
파일4

컴포넌트 파일 동적 임포트

앞서 소개한 공통 컴포넌트 외에 해당 마크다운 내에서만 사용하는 컴포넌트를 작성할 수 있다. fs 모듈을 사용하여 파일을 읽어오고, 파일 이름을 키로 하여 컴포넌트를 임포트한다.

tsx
// 커스텀 컴포넌트 파일 들 동적으로 임포트
// 존재하는 경우만 임포트
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를 포함한 디렉토리를 생성하고, 그 안에 컴포넌트 파일을 생성하면 된다.

app/
components/
mdx/
using-custom-components-in-serverside-mdx/
HelloWorld.tsx
tsx
export default function HelloWorld() {
    return <div className="text-2xl font-bold">Hello World ✨✨✨</div>;
}

MDX 파일에서 HelloWorld 컴포넌트를 사용할 수 있다.

Hello World ✨✨✨
MDX 파일에서 사용하기
mdx
<HelloWorld />