모던 리액트 + Next.js 시리즈에서 배운 Server Components / Server Actions를 실전 프로젝트로 손에 익혀봅니다. 이번 시리즈는 개인 블로그를 처음부터 만듭니다 — 흥미로운 점 하나는, 지금 이 글을 읽고 계신 사이트(schoolofweb.net) 자체가 거의 같은 구조라는 거예요. 도그푸딩 학습이 가능한 좋은 주제입니다.
5편으로 나눠 점진적으로 쌓아갑니다.
- #1 시작과 설계 ← 이번 글
- #2 글 목록과 상세 페이지
- #3 태그와 검색
- #4 댓글 (Server Actions)
- #5 SEO와 배포 (마무리)
요구사항 정의
먼저 우리가 만들 블로그가 뭘 할 수 있어야 하는지 적어둡니다.
핵심 기능
- 글 목록 페이지 (최신순)
- 글 상세 페이지 (마크다운 본문 렌더)
- 태그별 글 모음 페이지
- 검색 기능 (제목/본문)
- 글마다 댓글 달기
기술 결정 (앞에서 정해두면 흔들림 없음)
- 글 본문은 MDX 파일로 작성 (DB 없음, 파일 시스템에 저장)
- 댓글은 메모리 저장소 사용 (실제 서비스라면 DB가 필요하지만 학습 단순화 위해)
- 데이터 페칭은 Server Components가 직접 fs로 읽기
- mutation은 Server Actions 사용
- 배포는 Vercel
이 결정들이 시리즈 내내 중심을 잡아줍니다. "이건 어디서 다루나?", "이건 어떻게 풀어야 하나?"에 헤매지 않게 해주죠.
왜 MDX 파일 기반인가?
블로그를 만들 때 글 데이터를 어디에 둘지 큰 선택지 셋:
- DB (PostgreSQL, SQLite, Supabase 등) — 다인 운영 / 어드민 페이지 / 동적 글 추가에 강점
- MDX 파일 — Git 워크플로우, 단순함, JSX 컴포넌트 임베딩
- 외부 CMS (Contentful, Sanity 등) — 비기술자 편집자 협업에 강점
이 시리즈는 MDX 파일 방식을 택합니다. 이유:
- Git이 곧 백업 — 글 히스토리가 그대로 보존됨
- 로컬에서 편집 — 좋아하는 에디터로 글 쓰기
- DB 셋업 안 함 — 학습 부담 ↓
- Server Components의 강점이 자연스럽게 살아남 —
fs.readFileSync로 파일을 직접 읽는 코드가 그대로 동작 - React 컴포넌트 임베딩 — 글 안에
<YouTube />,<Tip>같은 커스텀 컴포넌트 사용 가능
이 사이트도 같은 방식이고, 많은 개인 기술 블로그가 이렇게 운영됩니다.
폴더 구조 설계
코딩 시작 전 폴더 구조를 그려보면 막히는 일이 줄어듭니다.
my-blog/
├── posts/ ← MDX 글들이 사는 곳
│ ├── hello-world.mdx
│ ├── about-rsc.mdx
│ └── learning-react.mdx
├── src/
│ └── app/
│ ├── layout.js ← 사이트 공통 (헤더/푸터)
│ ├── page.js ← '/' 글 목록 (최신순)
│ ├── posts/
│ │ └── [slug]/
│ │ └── page.js ← '/posts/[slug]' 글 상세
│ ├── tags/
│ │ ├── page.js ← '/tags' 태그 목록
│ │ └── [tag]/
│ │ └── page.js ← '/tags/[tag]' 태그별 글
│ ├── search/
│ │ └── page.js ← '/search?q=...' 검색
│ └── lib/
│ └── posts.js ← MDX 읽기/파싱 유틸
├── public/
└── package.json각 라우트의 역할:
| 라우트 | 화면 |
|---|---|
/ | 최신 글 목록 |
/posts/[slug] | 글 상세 (본문 + 댓글) |
/tags | 모든 태그 목록 |
/tags/[tag] | 특정 태그가 붙은 글들 |
/search?q=... | 검색 결과 |
src/app/lib/posts.js 같은 유틸 파일에 "MDX 파일 읽기" 같은 공통 로직을 모아둘 거예요. 페이지 파일이 너무 비대해지지 않도록 분리하는 흔한 패턴입니다.
글 데이터 모양 정하기
각 MDX 파일이 어떤 정보를 가질지 정해두면 코드가 깔끔해집니다.
---
title: "안녕, 블로그"
date: 2026-05-01
description: "첫 글입니다."
tags: ["일상", "공지"]
draft: false
---
## 첫 단락
이것은 **마크다운**으로 쓴 본문입니다.
리스트도 가능:
- 항목 1
- 항목 2
- 항목 3위쪽의 ---로 둘러싸인 부분이 frontmatter(메타데이터)이고, 그 아래가 본문입니다. frontmatter는 YAML 형식이며, 우리 코드에서 자바스크립트 객체로 파싱해 사용할 거예요.
각 필드의 의미:
| 필드 | 타입 | 설명 |
|---|---|---|
title | string | 글 제목 |
date | string (YYYY-MM-DD) | 발행일 |
description | string | 한 줄 요약 (목록과 메타에 사용) |
tags | string[] | 태그 배열 |
draft | boolean | true면 목록에 안 보임 (작성 중) |
이 외에도 필요하면 image, keywords 등을 추가할 수 있는데 일단 최소한으로 시작합시다.
Slug
/posts/[slug]의 slug는 글의 URL 식별자입니다. 우리는 파일명을 슬러그로 사용할 거예요.
posts/hello-world.mdx→/posts/hello-worldposts/about-rsc.mdx→/posts/about-rsc
규칙이 단순해서 좋습니다. 글마다 별도 ID 부여나 URL 매핑 작업이 필요 없어요.
프로젝트 시작
이제 진짜 코드로 들어갑니다. 새 Next.js 프로젝트를 만듭니다.
npx create-next-app@latest my-blog
cd my-blog지난 시리즈에서 했던 것과 같은 옵션 선택 (App Router, JavaScript, src/ directory 권장).
필요한 의존성
MDX 파일을 파싱하고 컴파일할 라이브러리를 설치합니다.
npm install gray-matter next-mdx-remote각 패키지의 역할:
gray-matter—.mdx파일에서 frontmatter와 본문을 분리next-mdx-remote— 본문 마크다운을 React 컴포넌트로 컴파일
선택적으로 추가할 만한 패키지:
remark-gfm— GitHub Flavored Markdown(테이블, 체크박스 등) 지원rehype-pretty-code— 코드 블록 신택스 하이라이팅
이번 글에서는 일단 핵심 두 개만 설치하고, #2에서 본문 컴파일하면서 추가 플러그인들을 도입하겠습니다.
첫 번째 글 만들기
my-blog/posts/ 폴더를 만들고 (Next.js 외부에 둡니다 — 라우트가 아닌 데이터니까요) 첫 글을 추가합니다.
posts/hello-world.mdx:
---
title: "안녕, 블로그"
date: 2026-05-01
description: "Next.js로 만든 첫 블로그 글입니다."
tags: ["공지", "리액트"]
draft: false
---
# 안녕하세요!
이 글은 **MDX**로 작성되었습니다. Next.js의 Server Component가 이 파일을 직접 읽어서 화면에 그립니다.
## 굵은 글씨, 기울임, 코드
마크다운 기본 문법 대부분이 동작합니다.
- 리스트 항목 1
- 리스트 항목 2
- 리스트 항목 3
`인라인 코드`도 가능합니다.
```js
// 코드 블록도
function hello() {
console.log("hello");
}
\```(주의: 위 본문의 마지막 코드 블록 끝부분 \``` 는 실제 파일에서는 `````` 로 적습니다 — 마크다운 인라인 표시상 이스케이프된 것)
posts/learning-react.mdx:
---
title: "리액트 학습 노트"
date: 2026-05-10
description: "리액트를 배우면서 정리한 핵심 포인트들."
tags: ["리액트", "학습"]
draft: false
---
리액트 공부 시작!
## Server Components
기본은 Server Component, 필요한 것만 Client Component.posts/draft-not-shown.mdx:
---
title: "아직 작성 중"
date: 2026-05-15
description: "초안입니다."
tags: ["메모"]
draft: true
---
이 글은 draft가 true라 목록에 안 나타나야 합니다.세 글 모두 비슷한 구조죠. draft: true인 글은 #2에서 만들 목록에서 자동 제외됩니다.
MDX 파싱 유틸 — 첫 단계
src/app/lib/posts.js에 MDX 파일을 읽고 파싱하는 함수들의 첫 윤곽을 만들어둡니다 (#2에서 본격 사용).
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';
const POSTS_DIR = path.join(process.cwd(), 'posts');
export function getAllSlugs() {
return fs.readdirSync(POSTS_DIR)
.filter(file => file.endsWith('.mdx'))
.map(file => file.replace(/\.mdx$/, ''));
}
export function getPostBySlug(slug) {
const fullPath = path.join(POSTS_DIR, `${slug}.mdx`);
if (!fs.existsSync(fullPath)) return null;
const fileContent = fs.readFileSync(fullPath, 'utf-8');
const { data, content } = matter(fileContent);
return {
slug,
frontmatter: data,
content,
};
}
export function getAllPosts() {
const slugs = getAllSlugs();
const posts = slugs
.map(slug => getPostBySlug(slug))
.filter(post => post && !post.frontmatter.draft);
return posts.sort((a, b) => a.frontmatter.date < b.frontmatter.date ? 1 : -1);
}각 함수의 역할:
getAllSlugs()—posts/폴더의.mdx파일 이름들을 슬러그 배열로 반환getPostBySlug(slug)— 슬러그에 해당하는 파일을 읽고 frontmatter와 본문 분리getAllPosts()— 모든 글을 가져오되 draft는 제외, 발행일 내림차순 정렬
이 함수들은 Server Component에서만 호출 가능합니다 (fs를 쓰니까요). Client Component에서 이걸 쓰려고 하면 빌드 에러가 납니다 — 의도한 대로의 안전장치예요.
이 사이트의 app/lib/posts-util.ts도 거의 같은 구조로 동작합니다. 다만 우리는 학습 목적이라 단순화해서 시작하고, 필요할 때 점진적으로 확장합니다.
동작 확인
지금 상태에서는 페이지가 아직 없으니 dev 서버를 띄워도 의미 있는 화면이 나오지 않습니다. 그래도 한 번 잘 돌아가는지는 확인해두면 좋아요.
npm run devhttp://localhost:3000에서 Next.js 기본 화면이 보이면 OK. 다음 글부터 진짜 페이지를 만들어갑니다.
마무리
이번 글에서는 블로그 빌드 시리즈의 토대를 다졌습니다.
- 요구사항을 명확히 적었다 (목록 / 상세 / 태그 / 검색 / 댓글)
- 데이터 저장 방식을 결정했다 (MDX 파일)
- 폴더 구조와 라우팅 그림을 그렸다
- 글 frontmatter 모양을 정했다
- 첫 MDX 글들을 만들었다
posts.js유틸 함수의 첫 윤곽을 잡았다
본격적인 화면 작업은 다음 글부터입니다. "Next.js로 블로그 만들기 #2 글 목록과 상세 페이지"에서는 위에서 만든 getAllPosts를 활용해 홈 화면에 글 목록을 그리고, /posts/[slug] 동적 라우트에서 MDX 본문을 컴파일해 표시하는 곳까지 갑니다.