블로그 만들기 01 – 구조 설계부터 사이드바 구현까지

2025-08-27

#프로젝트#블로그#티스토리#Nextjs#Vercel#Obsidian#Markdown

📌 프로젝트 개요

정적인 마크다운 기반 블로그를 직접 개발하며, 콘텐츠 디렉토리 구조를 그대로 반영하는 사이드바 탐색기, 코드블럭 하이라이팅, 목차 자동 추출 등의 기능을 구축하고 있다. 이 글에서는 개발 진행 과정에서 겪은 주요 이슈들과 그 해결 방법, 그리고 기술적 선택들을 정리할 예정이다.


🏗️ 1. 구조 설계

  • Next.js App Router 기반 구조

  • posts/ 디렉토리에 .md 파일을 계층적으로 저장하고 이를 기반으로 자동 탐색

  • getAllPosts() 함수를 통해 마크다운 frontmatter 파싱

  • buildDirectoryTree()로 실제 파일 구조를 기반으로 사이드바 트리 생성

디렉토리 기반 탐색을 그대로 시각화하여 관리자의 콘텐츠 관리 편의성과 사용자의 직관적 접근성을 모두 확보함


🧠 2. 주요 기능

2.1 마크다운 렌더링

  • react-markdown, remark-gfm, rehype-slug 조합으로 마크다운 렌더링

  • remarkRehype, rehypeStringifyunified를 통해 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-rehypeGFM 문법 지원 및 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카테고리 라우팅, 현재 위치 감지

진행 사항

사이드바, 게시글 목록

스크린샷 2025-08-27 17.36.48.png

사이드바, 게시글

스크린샷 2025-08-27 17.48.59.png

목차

스크린샷 2025-08-27 17.49.20.png

🔍 마무리

🌟 좋았던 점

  • 마크다운 기반 글 관리와 디렉토리 트리 UI의 결합으로 확장성과 유지보수성 향상
  • 직접 디렉토리를 파싱하고 구조화하는 과정에서 fs와 Node 환경에 대한 이해 증진
  • 사이드바 트리 UI에 간단한 애니메이션을 적용하여 사용자 경험 개선

😭 아쉬웠던 점

  • 디자인이 구리다...