Published on

Next.js로 마크다운 블로그 만들고 꾸미기

#🚀 Next.js로 마크다운 블로그 만들기

안녕하세요! 이 가이드에서는 Next.js마크다운을 사용해서 예쁜 블로그를 만드는 방법을 초보자도 이해할 수 있게 단계별로 설명하겠습니다.

#📋 목차

  1. 전체 구조 이해하기
  2. 필요한 라이브러리들
  3. 폴더 구조
  4. 데이터 처리 흐름
  5. 스타일링 시스템
  6. 실제 코드 설명
  7. 문제 해결 가이드

#🎯 전체 구조 이해하기

우리 블로그는 이렇게 동작합니다:

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;
}

#🎉 결론

이렇게 해서 마크다운 파일만 작성하면 자동으로 예쁜 블로그가 되는 시스템을 만들었습니다!

핵심은:

  1. 📝 간단한 글쓰기 (마크다운)
  2. 🔄 자동 변환 (marked + highlight.js)
  3. 🎨 예쁜 스타일링 (CSS)
  4. 🔗 완벽한 내비게이션 (목차 앵커 링크)

이제 data/blog/ 폴더에 .mdx 파일만 추가하면 블로그 글이 자동으로 나타납니다!

Happy Blogging! 🚀✨