소개
Next.js 에서 MDX 를 사용하면 정적 사이트를 쉽게 만들고 배포할 수 있습니다. 이번 글에서는 Next.js와 MDX 를 사용하여 블로그를 만드는 과정을 자세히 설명하겠습니다.
Next.js 와 MDX 를 선택한 이유
Next.js는 React 기반의 프레임워크로, 정적 사이트 생성(SSG)과 서버 사이드 렌더링(SSR)을 지원합니다. Next.js를 선택한 이유는 다음과 같습니다:
- 성능 최적화: Next.js는 정적 사이트 생성(SSG)을 통해 빠른 로딩 속도를 제공합니다.
- SEO 최적화: 서버 사이드 렌더링(SSR)을 통해 검색 엔진 최적화(SEO)에 유리합니다.
- 개발자 경험: Next.js는 파일 기반 라우팅, 자동 코드 분할, 핫 리로딩 등 개발자 경험을 향상시키는 기능을 제공합니다.
MDX는 Markdown과 JSX를 결합한 형식으로, Markdown의 간결함과 React 컴포넌트의 유연성을 동시에 활용할 수 있습니다. MDX를 선택한 이유는 다음과 같습니다:
- 유연성: MDX를 사용하면 Markdown 문서 내에서 React 컴포넌트를 사용할 수 있어, 동적인 콘텐츠를 쉽게 작성할 수 있습니다. (Next 와 아주 잘 맞습니다.)
- 재사용성: React 컴포넌트를 재사용하여 일관된 스타일과 기능을 유지할 수 있습니다.
- 확장성: MDX는 플러그인을 통해 기능을 확장할 수 있어, 다양한 요구사항을 충족할 수 있습니다. (저는 코드 플러그인을 확장해서 사용하여 코드를 하이라이팅 했습니다.)
프로젝트 설정
Next.js 프로젝트를 생성하고, 필요한 패키지를 설치하는 초기 설정을 설명합니다.
Next.js 프로젝트 설정
Next 프로젝트를 생성하기 위해서 create-next-app
명령어를 사용합니다.
npx create-next-app my-blog
명령어를 실행하면 my-blog 폴더에 next 프로젝트가 생성됩니다.
필요한 패키지 설치
Next.js 프로젝트에 MDX를 통합하기 위해서 필요한 패키지를 설치합니다.
npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx
초기 설정
Next.js 와 MDX 를 통합하기 위해서 next.config.js
파일을 수정합니다.
/next.config.js
import type { NextConfig } from "next";
import createMDX from "@next/mdx";
/** @type {import('next').NextConfig} */
const nextConfig: NextConfig = {
pageExtensions: ["js", "jsx", "md", "mdx", "ts", "tsx"],
};
const withMDX = createMDX({
// Add markdown plugins here, as desired
});
// Merge MDX config with Next.js config
export default withMDX(nextConfig);
위 설정을 통해서 MDX 파일을 Next.js 프로젝트에서 사용할 수 있게 됩니다. pagesExtensions
옵션은 확장자를 지정하여 지정된 옵션에 파일을 페이지로 인식될 수 있도록 합니다.
MDX 컴포넌트 생성
위에 설정은 MDX 파일을 Next 에서 page 로 인식할 수 있도록 합니다. MDX 에 적힌 컨텐츠를 react 의 컴포넌트로 표시하기 위해서 MDX 용 컴포넌트를 만들어야 합니다.
MDX 컴포넌트는 MDX 파일에 내용을 파싱하여 React 컴포넌트가 될 수 있도록 합니다.
/components/mdx.tsx
import { cn } from "@/utils";
import * as runtime from "react/jsx-runtime";
const components = {
a: ({ className, ...props }: React.HTMLAttributes<HTMLAnchorElement>) => (
<a target="_blank" rel="noopener" {...props} className={cn(className)} />
),
};
const useMDXComponent = (code: string) => {
const fn = new Function(code);
return fn({ ...runtime }).default;
};
interface Props {
code: string;
}
export function Mdx({ code }: Props) {
const Component = useMDXComponent(code);
return (
<div className="flex-1">
<Component components={components} />
</div>
);
}
위 코드에서 components 를 사용해서 MDX 컨텐츠를 원하는 컴포넌트로 커스텀 할 수 있습니다.
블로그 컨텐츠 작성
블로그 컨텐츠를 작성하고 Next 앱을 빌드하여 정적 사이트를 생성합니다. 먼저 posts 폴더를 생성하여 블로그 컨텐츠를 작성합니다.
폴더 구조
|--/
|--/app
|--/next.config.js
|--/posts // posts 폴더 생성
|----/content.mdx // blog 컨텐츠 추가
|--/components
|----/mdx.tsx
Velite 를 사용한 정적 JSON 생성
MDX 파일을 사용해서 이제 페이지를 만들 수 있습니다. 하지만 블로그 컨텐츠는 썸네일 정보, 작성일, 설명 등 메타데이터가 보통은 필요합니다. Velite 는 MDX 를 파싱하여 JSON 생성하는데 MDX 파일 맨 위에 테이블을 추가하여 메타데이터를 넣을 수 있습니다.
메타 데이터를 사용한 마크다운 파일은 다음 같은 형식이 됩니다.
/posts/content.mdx
---
title: Next.js 와 MDX 를 사용해서 SSG 블로그 만들기
date: 2025-02-09
thumbnailUrl: /posts/thumbnail/Next-Mdx-Blog.webp
description: 블로그를 만들고 배포하는 과정을 설명합니다.
tags: Next@_@MDX@_@AWS@_@Github Actions@_@
tab: tech
---
... (생략 나머지 컨텐츠)
Velite 를 사용하여 정적 JSON 을 생성하기 위해 velite 를 설치합니다.
npm install velite
그런 다음, Velite 설정 파일을 생성합니다. 이름은 velite.config.js
로 합니다.
velite.config.js
import rehypePrettyCode from "rehype-pretty-code";
import { defineConfig, s } from "velite";
export default defineConfig({
root: "posts",
collections: {
posts: {
name: "Post", // collection type name
pattern: "blog/*.mdx",
schema: s
.object({
title: s.string().max(99),
slug: s.path(),
date: s.string(),
description: s.string().max(199),
thumbnailUrl: s.string().max(99),
body: s.mdx(),
})
.transform((data) => {
return {
...data,
permalink: `/${data.slug}`,
slug: data.slug.replaceAll("blog/", ""),
};
}),
},
},
output: {
data: ".velite",
assets: "public/static",
base: "/static/",
name: "[name]-[hash:6].[ext]",
clean: true,
},
mdx: {
rehypePlugins: [[rehypePrettyCode, { theme: "nord" }]],
},
});
schema 옵션을 사용하여 메타 데이터에 스키마를 설정합니다. 이후 npx velite
를 실행하면 설정 파일을 토대로 MDX 파일을 읽은 후 JSON 파일이 생성됩니다.
빌드 시점마다 velite 가 실행 될 수 있도록 하기 위해서 저는 pacakge.json 에 스크립트를 추가했습니다.
/package.json
{
// ... 생략
"script": {
"predev": "velite",
"dev": "next dev",
"prebuild": "velite",
"build": "next build"
}
// ... 생략
}
velite 가 실행되면 .velite 폴더에 posts.json 이 생성됩니다. posts.json 에 내용은 배열로 meta 데이터 형식과 react 컴포넌트를 생성할 수 있는 코드로 구성된 body 로 구성됩니다.
./velite/posts.json
[
{
"title": "Next.js 와 MDX 를 사용해서 SSG 블로그 만들기",
"slug": "next-mdx-blog",
"date": "2025-02-09",
"description": "블로그를 만들고 배포하는 과정을 설명합니다.",
"thumbnailUrl": "/posts/thumbnail/Next-Mdx-Blog.webp",
"body": "...(react 컴포넌트가 될 수 있는 코드)...",
"permalink": "/blog/next-mdx-blog"
}
]
Page 생성
posts.json 을 사용하여 Next 페이지를 생성합니다. velite 로 생성된 json 을 활용해서 url endpoint 가 될 수 있도록 dynamic route 를 사용합니다.
src/app/blog/[slug]/page.tsx
export default async function BlogDetailPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const slug = (await params).slug;
const post = getPost({ slug });
return (
<div className="py-6">
<h1 className="text-brand-800 text-2xl sm:text-[3rem] font-bold tracking-tighter leading-tight pb-2 sm:pb-4">
{post.title}
</h1>
<p className="text-xl sm:text-2xl font-semibold pb-2 sm:pb-4">
{post.description}
</p>
<time className="text-slate-600 font-semibold">{post.date}</time>
<article className="blog-post-container pt-4 sm:pt-6">
<Mdx code={post.body} />
</article>
</div>
);
}
export async function generateStaticParams() {
const slugs = posts.map((post) => ({
slug: post.slug,
}));
return slugs;
}
function getPost({ slug }: { slug: string }) {
const post = posts.find((post) => post.slug === slug);
if (!post) {
throw new Error(`Post not found: ${slug}`);
}
return post;
}
배포
AWS 를 사용해서 배포했습니다. S3 버킷을 생성하여 정적 호스팅을 할 수 있도록 구성한 후 Cloud Front 에 Origin 이 S3 를 바라보도록 진행합니다.
S3 에 올려서 정적 사이트를 배포하기 위해서 html 빌드 결과를 만드는 작업이 필요합니다. output 에 결과가 html 파일이 될수 있또록 하려면 next.config.ts
파일을 수정해야 하는데 다음과 같습니다.
next.config.ts
import type { NextConfig } from "next";
import createMDX from "@next/mdx";
/** @type {import('next').NextConfig} */
const nextConfig: NextConfig = {
images: {
unoptimized: true, // export 모드에서 Next 에 Image 최적화가 동작하지 않기 때문에 unOptimized 를 true 로 설정합니다. ❗️
},
output: process.env.NODE_ENV === "development" ? "standalone" : "export", // production 환경일때 export 가 되게 합니다.❗️
trailingSlash: true, // blog/index.html 이 되도록 합니다.❗️
};
먼저 output 모드를 export 로 설정하여 빌드 결과로 html 파일을 생성할수 있도록 합니다. 다음으로, export 모드에서는 따로 서버가 있는것이 아니기 때문에 이미지 최적화가 동작하지 않으므로 images.unoptimzed 를 true 로 설정합니다. trailingSlash 를 true 로 설정하여 page 이름에 폴더를 생성후 index.html 을 만들 도록 합니다. 예를 들어 이 옵션을 사용하지 않으면 빌드 결과가 /blog/index.html 이 아닌 /blog.html 이 됩니다. 보통 url 을 통해 웹 사이트를 접근할 때 /blog.html 이 아닌 /blog 로 접근하기 때문에 이 옵션을 키도록 합니다.
next build
로 build 후 결과를 s3 에 업로드 합니다.
정리
이런 방법을 사용하면 MDX 문서를 작성 하는것 만으로 Nexts 에 Page 를 구성할 수 있기 때문에 블로그 같은 정적 사이트를 쉽게 개발 할 수 있습니다. 정적 호스팅에 대한 비용도 AWS 기준으로 굉장히 저렴하기 때문에 비용 적인 측면에서도 이점이 있었습니다.
실제로 호스팅 하기 위해서 개발 외적인 부분에 신경 쓸 부분도 많았지만, 간단한 구성으로 MPA 와 SPA 에 장점을 모두 살릴 수 있기 때문에 유용하다고 생각됩니다.