Next.js + MDX 블로그 만들기 - 1
1. 블로그를 만들어야겠다고 생각한 이유
지금까지 기술을 정리하거나 회고를 할 때 노션을 주로 사용해왔지만,
정리한 내용을 검색하기 어렵고, 다른 사람들과 공유하기도 애매했다.
또, 최근에는 실무와 면접 준비 과정에서
"내가 어떤 생각을 하고 어떻게 풀었는가”를 남겨두는 것이 중요하다는 걸 절실히 느꼈다.
그래서 기록을 쌓을 수 있는 나만의 공간이 필요하다고 생각했다.
물론 지금 글을 적고 있는 Tistory도 충분히 글을 풀어내는 데에는 좋은 공간이지만,
직접 코드를 실행하고 보여줄 수 있는 공간으로써는 부족함을 많이 느꼈던 것 같다.
그래서 실제로 코드를 실행하는 것까지 보여줄 수 있는 환경을 구축하기 위해
블로그를 직접 만들어보기로 결정했다.
같이 스터디를 했던 분들도 이런 고민에 대한 일환으로 블로그를 만들고 계셨기에
좋은 참고 자료가 되기도 했다.
2. Next.js를 선택한 이유와 폴더 구조
처음에는 Astro나 Gatsby 같은 정적 사이트 생성기들도 고려했지만,
결국에는 실무에서 가장 많이 사용하는 Next.js로 결정했다.
그 이유는 다음과 같다:
- React 기반이라 친숙하고, 컴포넌트 단위 구성에 익숙함
- App Router + Server Component 환경을 실험해보고 싶었음
- MDX 지원이 점점 더 좋아지고 있음
- Express 백엔드와 연결해서 댓글 기능까지 붙이고 싶었음
기본적인 폴더 구조는 아래와 같이 구성했다:
blog/
├── app/
│ ├── layout.tsx
│ └── blog/[slug]/page.tsx
├── components/
│ ├── navbar.tsx
│ └── theme-toggle.tsx
├── content/
│ └── posts/
│ └── 첫글.mdx
├── lib/
│ └── mdx.ts
├── styles/
│ └── globals.css
├── tailwind.config.ts
├── postcss.config.js
└── next.config.ts
🧩 설명: app/에는 App Router 기반 페이지 구성, components/에는 재사용 가능한 UI 컴포넌트, content/는 MDX 글, lib/은 유틸 함수, styles/는 글로벌 스타일을 담당한다.
3. Tailwind CSS 적용 중 겪은 문제와 해결 방법
Tailwind를 적용하면서 가장 시간을 많이 잡아먹었던 부분이 바로 CSS가 적용되지 않는 문제였다.
특히 Tailwind CSS v4부터 내부 구조가 변경되면서 다음과 같은 착각을 했다:
// ❌ 이렇게 하면 안 됨
const config = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
⚠️ 문제: 공식 문서를 오해해서 tailwindcss가 정상적으로 로드되지 않았음
공식 문서를 다시 확인한 결과, v4에서도 기존처럼 @tailwind는 그대로 사용 가능했으며
문제의 핵심은 PostCSS 설정에서 모듈을 잘못 불러온 것이었다.
최종적으로 아래와 같이 설정을 고쳐 해결했다:
// ✅ 해결된 postcss.config.mjs
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
/* ✅ globals.css */
@import "tailwindcss";
그리고 .next 캐시 폴더를 지운 뒤 npm run dev를 하니 정상 작동했다! 🎉
4. MDX 적용 과정
이 블로그의 핵심은 Markdown + JSX 조합인 MDX로 글을 작성하는 것이었다.
Next.js App Router에서 공식적으로 지원되는 next-mdx-remote/rsc 기반으로 구성했다.
// lib/mdx.ts
// ✨ 파일 목록을 읽어와서 메타데이터 추출
export function getAllPosts(): BlogPostMeta[] {
return fs.readdirSync(POSTS_PATH).map((filename) => {
const filePath = path.join(POSTS_PATH, filename);
const source = fs.readFileSync(filePath, "utf8");
const { data } = matter(source);
return {
slug: filename.replace(".mdx", ""),
title: data.title,
date: data.date,
summary: data.summary,
};
});
}
// app/blog/[slug]/page.tsx
// ✨ 각 블로그 페이지에서 MDX를 읽어와 렌더링
export default async function BlogPostPage({ params }: Params) {
const filePath = path.join(process.cwd(), "content/posts", `${params.slug}.mdx`);
const source = fs.readFileSync(filePath, "utf-8");
const { content, frontmatter } = await compileMDX({
source,
options: { parseFrontmatter: true },
});
return (
<article className="prose dark:prose-invert p-6 max-w-2xl mx-auto">
<h1>{frontmatter.title}</h1>
<p className="text-gray-500 text-sm">{frontmatter.date}</p>
{content}
</article>
);
}
예시 파일:
---
title: 안녕하세요!
date: 2025-05-31
summary: 첫 블로그 글입니다.
---
# 반갑습니다 👋
이 블로그는 MDX로 만들어졌습니다.
5. Docker로 배포까지 완료
모든 개발이 끝난 후, Docker로 컨테이너화해 서버에 배포했다.
도커파일은 다음과 같다:
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
그리고 nginx를 이용해 hyunu-blog.kro.kr 도메인을 연결하고,
3001번 포트로 프록시 설정을 하여 외부에서 접근 가능하게 구성했다.
❌ 문제 상황: 처음에는 node:18-alpine을 사용했는데,
내부적으로 사용하는 lightningcss 바이너리가 alpine-musl 환경에서 작동하지 않았다.✅ 해결 방법: node:18-slim 이미지로 변경.
glibc 기반이라 lightningcss가 정상 작동함.
✅ 앞으로의 계획
- 🇰🇷🇺🇸 다국어 지원 (한국어/영어 전환)
- 💬 댓글 기능 추가
- 📨 구독 시스템 도입
🔚 마무리
이제 기본적인 블로그는 완성되었고,
하나하나 기록을 남기면서 다시 돌아봤을 때 내가 어떻게 성장해왔는지 확인할 수 있는 공간으로 만들고자 한다.