- Published on
Next.js로 마크다운 블로그 만들고 꾸미기
#🚀 Next.js로 마크다운 블로그 만들기
안녕하세요! 이 가이드에서는 Next.js와 마크다운을 사용해서 예쁜 블로그를 만드는 방법을 초보자도 이해할 수 있게 단계별로 설명하겠습니다.
#📋 목차
#🎯 전체 구조 이해하기
우리 블로그는 이렇게 동작합니다:
1. 📝 .mdx 파일 작성 (data/blog/)
↓
2. 🔄 라이브러리가 파일 읽기 (lib/mdx.ts)
↓
3. 🎨 HTML로 변환 + 스타일 적용
↓
4. 📱 웹페이지로 출력
쉽게 말하면: 마크다운으로 글을 쓰면 → 자동으로 예쁜 웹페이지가 된다!
#📦 필요한 라이브러리들
우리가 설치한 라이브러리들과 각각의 역할:
#1. 핵심 라이브러리
npm install marked gray-matter highlight.js
marked: 마크다운 → HTML 변환기gray-matter: 파일 맨 위 정보(제목, 날짜) 읽기highlight.js: 코드에 예쁜 색깔 입히기
#2. 각 라이브러리가 하는 일
#`marked` (마크다운 변환기)
// 이런 마크다운을
'# 제목\n**굵은글씨**'
// 이런 HTML로 바꿔줌
'<h1>제목</h1><p><strong>굵은글씨</strong></p>'
#`gray-matter` (메타데이터 분리기)
// 파일 전체에서
---
title: '제목'
date: '2024-12-02'
---
본문 내용...
// 이렇게 분리해줌
data: { title: '제목', date: '2024-12-02' }
content: '본문 내용...'
#`highlight.js` (코드 하이라이팅)
// 코드 블록을
console.log('hello')
// 이렇게 예쁘게 만들어줌 (색깔 입히기)
<pre><code class="hljs">
<span class="hljs-built_in">console</span>...
</code></pre>
#📁 폴더 구조
프로젝트/
├── 📁 data/blog/ # 📝 블로그 글들 (.mdx 파일)
├── 📁 lib/ # 🔧 유틸리티 함수들
│ └── mdx.ts # 마크다운 처리 로직
├── 📁 app/blog/ # 📱 블로그 페이지들
│ ├── page.tsx # 목록 페이지
│ └── [...slug]/ # 개별 글 페이지
├── 📁 css/ # 🎨 스타일 파일들
│ ├── blog.css # 블로그 전용 스타일
│ └── tailwind.css # 전역 스타일
└── 📁 layouts/ # 🖼️ 레이아웃 컴포넌트
└── PostLayout.tsx # 글 페이지 레이아웃
#🔄 데이터 처리 흐름
#1단계: 파일 읽기
// lib/mdx.ts에서
function getAllPosts() {
const fileNames = fs.readdirSync('data/blog') // 📁 폴더에서 파일 목록 가져오기
return fileNames
.filter(name => name.endsWith('.mdx')) // 📝 .mdx 파일만 필터링
.map(fileName => {
const content = fs.readFileSync(...) // 📄 파일 내용 읽기
const { data, content } = matter(content) // ✂️ 메타데이터와 본문 분리
return { slug, content, ...data } // 📦 정리해서 반환
})
}
#2단계: HTML 변환
function getPostHtml(slug) {
const post = getPostBySlug(slug) // 📝 특정 글 가져오기
return marked(post.content) // 🔄 마크다운 → HTML 변환
}
#3단계: 웹페이지에 표시
// app/blog/[...slug]/page.tsx에서
export default function BlogPost({ params }) {
const htmlContent = getPostHtml(params.slug) // 📄 HTML 가져오기
return (
<div
className="blog-content" // 🎨 스타일 클래스 적용
dangerouslySetInnerHTML={{ __html: htmlContent }} // 🖼️ HTML 렌더링
/>
)
}
#🎨 스타일링 시스템
#1. CSS 계층 구조
📄 tailwind.css (전역 스타일)
↓ @import
📄 blog.css (블로그 전용 스타일)
↓ 적용 대상
🌐 .blog-content 클래스가 있는 요소
#2. blog.css에서 하는 일
/* 제목들 */
.blog-content h1 { color: #ffffff; font-size: 2.25rem; ... }
.blog-content h2 { color: #ffffff; font-size: 1.875rem; ... }
/* 본문 텍스트 */
.blog-content p { color: #d1d5db; line-height: 1.75; ... }
/* 코드 블록 */
.blog-content pre {
background-color: #1f2937;
border-radius: 0.5rem;
padding: 1rem;
}
/* 테이블 */
.blog-content table {
border: 1px solid #374151;
border-radius: 0.5rem;
}
#3. 스타일 적용 과정
1. 📝 마크다운: # 제목
↓
2. 🔄 HTML 변환: <h1>제목</h1>
↓
3. 🎨 CSS 적용: .blog-content h1 스타일 규칙 적용
↓
4. 🌐 브라우저: 흰색 큰 글씨로 표시
#💻 실제 코드 설명
#파일 1: `lib/mdx.ts` (핵심 로직)
import { marked } from 'marked'
import matter from 'gray-matter'
import hljs from 'highlight.js'
// marked 설정 (마크다운 → HTML 변환 규칙)
marked.setOptions({
highlight: function(code, lang) {
// 코드에 색깔 입히는 함수
return hljs.highlight(code, { language: lang }).value
}
})
// 모든 블로그 글 가져오기
export function getAllPosts() {
const files = fs.readdirSync('data/blog') // 파일 목록
return files
.filter(name => name.endsWith('.mdx')) // .mdx만 선택
.map(fileName => {
const content = fs.readFileSync(...) // 파일 읽기
const { data, content } = matter(content) // 분리
return { slug, content, ...data } // 정리
})
}
// HTML로 변환
export function getPostHtml(slug) {
const post = getPostBySlug(slug)
return marked(post.content) // 🔄 핵심: 마크다운 → HTML
}
#파일 2: `app/blog/page.tsx` (목록 페이지)
import { getAllPosts } from '@/lib/mdx'
export default function BlogPage() {
const posts = getAllPosts() // 📚 모든 글 가져오기
return (
<div>
{posts.map(post => (
<div key={post.slug}>
<h2>{post.title}</h2> {/* 제목 */}
<p>{post.summary}</p> {/* 요약 */}
<Link href={`/blog/${post.slug}`}>읽기</Link>
</div>
))}
</div>
)
}
#파일 3: `app/blog/[...slug]/page.tsx` (개별 글 페이지)
import { getPostHtml } from '@/lib/mdx'
export default function BlogPost({ params }) {
const htmlContent = getPostHtml(params.slug) // HTML 가져오기
return (
<div
className="blog-content" {/* 🎨 스타일 적용 */}
dangerouslySetInnerHTML={{ __html: htmlContent }} {/* HTML 렌더링 */}
/>
)
}
#🎯 전체 동작 순서 정리
#사용자가 글을 볼 때:
1. 🌐 사용자가 /blog/제목 접속
↓
2. 📁 Next.js가 app/blog/[...slug]/page.tsx 실행
↓
3. 🔧 getPostHtml('제목') 함수 호출
↓
4. 📄 data/blog/제목.mdx 파일 읽기
↓
5. ✂️ gray-matter로 메타데이터와 본문 분리
↓
6. 🔄 marked로 마크다운 → HTML 변환
↓
7. 🎨 highlight.js로 코드 블록에 색깔 적용
↓
8. 🌐 blog.css 스타일 적용해서 예쁘게 표시
#블로그 글을 추가할 때:
1. 📝 data/blog/새글.mdx 파일 생성
2. 🔝 맨 위에 메타데이터 작성 (---)
3. ✍️ 마크다운으로 본문 작성
4. 💾 저장
5. 🔄 자동으로 블로그에 나타남!
#✨ 왜 이렇게 만들었을까?
#장점들:
- 😊 쉬운 글쓰기: 마크다운만 알면 OK
- 🚀 빠른 속도: 정적 파일이라 빠름
- 🎨 예쁜 스타일: CSS로 완전 커스터마이징
- 💻 코드 하이라이팅: 자동으로 예쁜 코드 블록
- 📱 반응형: 모든 기기에서 잘 보임
#다른 방법들과 비교:
- WordPress: 무겁고 복잡
- Notion: 커스터마이징 한계
- 우리 방식: 가볍고 자유도 높음 ✅
#🛠️ 커스터마이징 팁
#새로운 스타일 추가하기:
/* css/blog.css에 추가 */
.blog-content .highlight-box {
background-color: #fbbf24;
padding: 1rem;
border-radius: 0.5rem;
}
#새로운 메타데이터 추가하기:
// lib/mdx.ts에서 BlogPost 인터페이스 수정
export interface BlogPost {
slug: string
content: string
title: string
date: string
author?: string // 새로 추가!
category?: string // 새로 추가!
}
#🔧 문제 해결 가이드
#목차 링크가 작동하지 않는 경우
문제 증상: 목차에서 링크를 클릭해도 해당 섹션으로 이동하지 않음
원인과 해결방법:
#1. marked v16+ API 변경 이슈
// ❌ 예전 방식 (작동하지 않음)
renderer.heading = function (text, level) {
const id = slugger(text)
return `<h${level} id="${id}">${text}</h${level}>`
}
// ✅ 새로운 방식 (marked v16+)
renderer.heading = function ({ text, depth }) {
const cleanText = typeof text === 'string' ? text : text.toString()
const id = slugger(cleanText)
return `<h${depth} id="${id}" class="content-header">
<a href="#${id}" class="content-header-link">#</a>
${cleanText}
</h${depth}>`
}
#2. 한글+이모지 헤딩 ID 생성 문제
// 헤딩: "🎯 전체 구조 이해하기"
// github-slugger 결과: "-전체-구조-이해하기" (이모지로 인해 앞에 - 추가)
// 목차 링크를 맞춰서 수정해야 함
[전체 구조 이해하기](#-전체-구조-이해하기) // ✅ 올바름
[전체 구조 이해하기](#전체-구조-이해하기) // ❌ 작동 안함
#3. 디버깅 방법
// lib/mdx.ts에 임시 로그 추가
renderer.heading = function ({ text, depth }) {
const cleanText = typeof text === 'string' ? text : text.toString()
const id = slugger(cleanText)
console.log(`Heading: "${cleanText}" -> ID: "${id}"`) // 디버깅용
return `<h${depth} id="${id}">${cleanText}</h${depth}>`
}
개발 서버 실행 후 브라우저에서 페이지를 열면 콘솔에 실제 생성되는 ID를 확인할 수 있습니다.
#4. CSS 스타일링 추가
/* 헤딩 링크 스타일 */
.blog-content h1,
.blog-content h2,
.blog-content h3,
.blog-content h4 {
position: relative;
}
.blog-content .content-header-link {
position: absolute;
left: -20px;
opacity: 0;
color: #6b7280;
text-decoration: none;
transition: opacity 0.2s ease;
}
.blog-content h1:hover .content-header-link,
.blog-content .content-header-link:hover {
opacity: 1;
}
#🎉 결론
이렇게 해서 마크다운 파일만 작성하면 자동으로 예쁜 블로그가 되는 시스템을 만들었습니다!
핵심은:
- 📝 간단한 글쓰기 (마크다운)
- 🔄 자동 변환 (marked + highlight.js)
- 🎨 예쁜 스타일링 (CSS)
- 🔗 완벽한 내비게이션 (목차 앵커 링크)
이제 data/blog/ 폴더에 .mdx 파일만 추가하면 블로그 글이 자동으로 나타납니다!
Happy Blogging! 🚀✨