블로그 만들기 01 – 구조 설계부터 사이드바 구현까지
2025-08-27
📌 프로젝트 개요
정적인 마크다운 기반 블로그를 직접 개발하며, 콘텐츠 디렉토리 구조를 그대로 반영하는 사이드바 탐색기, 코드블럭 하이라이팅, 목차 자동 추출 등의 기능을 구축하고 있다. 이 글에서는 개발 진행 과정에서 겪은 주요 이슈들과 그 해결 방법, 그리고 기술적 선택들을 정리할 예정이다.
🏗️ 1. 구조 설계
-
Next.js App Router 기반 구조
-
posts/디렉토리에.md파일을 계층적으로 저장하고 이를 기반으로 자동 탐색 -
getAllPosts()함수를 통해 마크다운 frontmatter 파싱 -
buildDirectoryTree()로 실제 파일 구조를 기반으로 사이드바 트리 생성
디렉토리 기반 탐색을 그대로 시각화하여 관리자의 콘텐츠 관리 편의성과 사용자의 직관적 접근성을 모두 확보함
🧠 2. 주요 기능
2.1 마크다운 렌더링
-
react-markdown,remark-gfm,rehype-slug조합으로 마크다운 렌더링 -
remarkRehype,rehypeStringify와unified를 통해 HTML 추출 -
<Content />컴포넌트로 전체 글 렌더링
import Markdown from "react-markdown"; import remarkGfm from "remark-gfm"; import remarkDirective from "remark-directive"; import remarkFrontmatter from "remark-frontmatter"; import rehypeFormat from "rehype-format"; import rehypeSanitize from "rehype-sanitize"; import rehypeSlug from "rehype-slug"; import rehypeRaw from "rehype-raw"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { oneDark } from "react-syntax-highlighter/dist/cjs/styles/prism"; import { Post } from "app/_lib/post"; interface ContentProps { post: Post; } const ContentMarkdown = ({ post }: ContentProps) => { return ( <Markdown remarkPlugins={[remarkGfm, remarkDirective, remarkFrontmatter]} rehypePlugins={[rehypeRaw, rehypeSlug, rehypeFormat, rehypeSanitize]} components={{ h1: ({ children, ...props }) => ( <h1 className="text-3xl font-extrabold mt-10 mb-6 border-b pb-2" {...props} > {children} </h1> ), h2: ({ children, ...props }) => ( <h2 id={children?.toString()} className="text-2xl font-bold mt-8 mb-4 border-b pb-1" {...props} > {children} </h2> ), h3: ({ children, ...props }) => ( <h3 id={children?.toString()} className="text-xl font-semibold mt-6 mb-2" {...props} > {children} </h3> ), p: ({ children, ...props }) => ( <p className="leading-relaxed mb-4" {...props}> {children} </p> ), strong: ({ children }) => ( <strong className="font-semibold text-foreground">{children}</strong> ), em: ({ children }) => ( <em className="italic text-muted-foreground">{children}</em> ), code: ({ className, children, ...props }) => { const match = /language-(\w+)/.exec(className || ""); const language = match?.[1]?.toLowerCase(); return language ? ( <SyntaxHighlighter style={oneDark} language={language} PreTag="div" customStyle={{ borderRadius: "8px", padding: "1rem", fontSize: "0.875rem", lineHeight: "1.5", marginBottom: "1.5rem", }} > {String(children).replace(/\n$/, "")} </SyntaxHighlighter> ) : ( <code className="bg-muted px-1 py-0.5 rounded text-sm" {...props}> {children} </code> ); }, blockquote: ({ children }) => ( <blockquote className="border-l-4 border-border pl-4 italic text-muted-foreground my-4"> {children} </blockquote> ), ul: ({ children }) => ( <ul className="list-disc list-inside ml-4 mb-4 space-y-1"> {children} </ul> ), ol: ({ children }) => ( <ol className="list-decimal list-inside ml-4 mb-4 space-y-1"> {children} </ol> ), li: ({ children }) => <li className="leading-relaxed">{children}</li>, table: ({ children }) => ( <div className="overflow-x-auto my-4"> <table className="table-auto border border-border w-full"> {children} </table> </div> ), thead: ({ children }) => ( <thead className="bg-muted text-left">{children}</thead> ), th: ({ children }) => ( <th className="p-2 border border-border font-semibold">{children}</th> ), td: ({ children }) => ( <td className="p-2 border border-border">{children}</td> ), hr: () => <hr className="my-8 border-border" />, }} > {post.content} </Markdown> ); }; export default ContentMarkdown;
2.2 코드블럭 하이라이팅
-
react-syntax-highlighter+one-dark테마 적용 -
inline code와 블럭 코드 구분하여 스타일링
-
language-*class 파싱으로 언어별 구분 렌더링
2.3 자동 목차 추출 (TOC)
-
rehype-slug를 통해<h2>,<h3>등에id부여 -
커스텀 파서
extractTOCFromHTML()로 HTML 문자열에서 제목 추출 -
사이드 고정형
<TOC />컴포넌트에서 스크롤 감지로 현재 위치 하이라이트
🧩 3. 디렉토리 트리 기반 사이드바 탐색기
✔️ 목표
-
posts/내부 디렉토리 구조 그대로 트리로 시각화 -
최대 depth=2까지 탐색 (3단계부터는 무시)
-
.md파일은 트리에는 표시하지 않고 count만 누적
✔️ 해결 과정
① fs 사용 불가 에러
-
fs는 Node.js 전용 모듈 →use client로 컴포넌트에서 직접 호출 시Can't resolve 'fs'오류 발생 -
👉 트리 로직은 서버 컴포넌트에서만 처리 (
SidebarContent에서 서버에서tree생성 후 클라이언트로 전달)
② 디렉토리만 포함하도록 정제
if (entry.isDirectory()) { const children = depth < 1 ? walk(fullPath, depth + 1) : undefined; const count = countMarkdownFiles(fullPath); tree.push({ name: entry.name, count, children: children?.length ? children : undefined, }); }
③ depth=1에서 children 없는 경우 toggle 없이 표시
- children이 없을 경우
대신 바로 로 렌더링
④ 아이콘 toggle 애니메이션
-
ChevronRightIcon → rotate-90 → ChevronDownIcon으로 변경 애니메이션 적용
-
children 영역은 max-h-0 ↔ max-h-[1000px]으로 부드러운 슬라이드
🛠️ 4. 기술 스택 및 라이브러리
| 항목 | 사용 기술 / 라이브러리 | 설명 |
|---|---|---|
| 마크다운 파싱 | react-markdown, remark-gfm, unified, remark-rehype | GFM 문법 지원 및 HTML 변환 |
| 하이라이팅 | react-syntax-highlighter, one-dark | 코드 블럭 스타일링 및 언어별 색상 적용 |
| 디렉토리 트리 탐색 | Node.js fs, path, gray-matter | 실제 디렉토리 기반 탐색, frontmatter 파싱 |
| 목차 자동 추출 | rehype-slug, 커스텀 extractTOCFromHTML 함수 | heading에 id 부여 후 TOC 추출 |
| UI 프레임워크 | TailwindCSS, @heroicons/react | 정적 스타일링, 아이콘 |
| 사이드바 트리 | 커스텀 트리 구조 컴포넌트 | depth 2까지 탐색, toggle 애니메이션 포함 |
| 애니메이션 효과 | transition-all, rotate, max-h-0/max-h-[value] | slide 효과 및 아이콘 회전 |
| 경로 관리 | Next.js App Router, usePathname | 카테고리 라우팅, 현재 위치 감지 |
진행 사항
사이드바, 게시글 목록
사이드바, 게시글
목차
🔍 마무리
🌟 좋았던 점
- 마크다운 기반 글 관리와 디렉토리 트리 UI의 결합으로 확장성과 유지보수성 향상
- 직접 디렉토리를 파싱하고 구조화하는 과정에서
fs와 Node 환경에 대한 이해 증진 - 사이드바 트리 UI에 간단한 애니메이션을 적용하여 사용자 경험 개선
😭 아쉬웠던 점
- 디자인이 구리다...