[blog - 9] Markdown 변환부터 메타데이터 분리까지 블로그 구조 개선 기록

Next.js성능 개선빌드 파이프라인Markdown메타데이터

게시글이 109개까지 늘어나면서 /blog 목록 페이지 로딩 속도가 761ms까지 늘었다. 원인은 페이지를 열 때마다 브라우저가 Markdown을 HTML로 바꾸고 있었기 때문이다.

문제 0 — 런타임 Markdown 변환이 누적 비용이 됐다

초기 구조는 페이지에서 렌더링(런타임)할 때마다 클라이언트가 Markdown → HTML 변환을 수행하는 구조였다.

Code
/content/posts/{카테고리}/{slug}.md

측정 결과

  • 총 Markdown 파일: 109개 (332KB)
  • /blog 목록 페이지 로딩: 761ms
  • 단일 게시글 MD → HTML 변환: 27ms
  • 글이 길수록 변환 비용이 커져서 체감 렉이 생겼다

개선 1 — 변환을 빌드 타임으로 옮겼다

첫 번째 선택은 “런타임 비용을 없애자”였다.

  • 빌드 단계에서 Markdown → HTML 변환
  • 결과를 JSON으로 저장
  • 런타임은 이미 변환된 결과를 렌더링만 한다
JSON
// /build/posts/{category}/{slug}.json
{
  "title": "...",
  "slug": "...",
  "date": "...",
  "category": "...",
  "content": "<h1>...(긴 HTML)...</h1>"
}

측정 결과

  • 변환 스크립트 전체 시간: 5.5s
  • /blog 목록 페이지: 24.2ms (761ms → 24.2ms)
  • 단일 게시글 로딩: 0.2ms (27ms → 0.2ms)

런타임 병목은 해결 되었다.

개선 후의 문제 1 — JSON이 너무 무거웠다

속도는 해결됐는데 모든 데이터(메타데이터 + HTML)를 한 JSON 파일에 넣다 보니 구조적 비효율이 생겼다.

문제 1) 메타만 필요해도 HTML까지 같이 읽는다

목록 페이지는 사실 제목/날짜/카테고리만 필요하다. 근데 JSON에 HTML 문자열이 통째로 들어있어서, 결국 전부 읽고 파싱해야 했다.

  • “모든 게시글을 읽는 데” 22.5ms (초기 755ms보단 빠르지만, 불필요한 작업이 포함된 수치였다)

문제 2) 조회 단위를 쪼갤 수가 없다

“목록”과 “상세”는 필요한 데이터가 다른데, 데이터 구조가 그 차이를 허용하지 않았다. 빠르긴 한데, 계속 이 구조로 가면 확장할수록 비용이 쌓일 게 보였다.

개선 2 — HTML과 메타데이터를 완전히 분리했다

이번 개선의 목표는 데이터 분리다.

  • 목록: 메타데이터만 읽는다
  • 상세: HTML은 필요할 때만 가져온다

구조를 이렇게 바꿨다.

HTML 파일

Code
/content/build/posts/{카테고리}/{게시글}.html

메타데이터(하나로 합친 JSON)

Code
/content/build/meta/posts.json

측졍 결과

  • 변환 스크립트 시간: 3.9s
  • 모든 게시글 메타데이터 로딩: 16.8ms
  • /blog 목록 페이지: 18.6ms
  • 상세보기 버튼 요청(API Router): 90.2ms

개선 후의 문제 2 — 메타데이터가 다시 하나로 뭉쳤다

HTML은 분리했는데, 메타데이터를 posts.json 하나로 합쳐둔 게 문제였다.

문제 1) 글이 늘어날수록 posts.json이 계속 커진다

게시글이 추가될 때마다 네트워크로 가져와야 하는 기본 덩어리가 커진다. 목록 페이지의 “기본 비용”이 콘텐츠 양에 비례해서 증가하는 구조가 된다.

문제 2) 필요한 범위만 가져올 수 없다

예를 들어 React 카테고리만 필요해도:

  • posts.json 전체 fetch
  • 전체 JSON 파싱
  • 카테고리 필터링

지금은 괜찮더라도 좋지 않은 구조라고 생각했다.

최종 개선 — 메타데이터를 카테고리 단위로 분산했다

마지막 선택은 “필요한 만큼만 가져오게 하자”였다.

최종 메타데이터 구조

Code
/content/build/meta/posts/{카테고리}/{slug}.json

HTML은 그대로 유지했다.

Code
/content/build/posts/{카테고리}/{slug}.html

추가로 카테고리/시리즈는 별도 파일에서 관리했다.

  • category.json
  • series.json

측정 결과는 이렇다.

  • 스크립트 전체 실행시간: 4s
  • 모든 게시글 조회: 18.5ms
  • /blog 목록 페이지: 20.3ms
  • 상세보기 버튼 요청: 87.1ms
  • 단일 게시글 조회: 0.2ms

1~2차 개선처럼 드라마틱한 속토의 차이는 없었지만 확장성 측면에서 더 큰 이점이 생겼다.

  • 카테고리 목록은 카테고리만 읽으면 된다
  • 특정 카테고리 목록도 그 범위만 읽으면 된다
  • 메타데이터의 기본 다운로드 단위가 작아졌다

성능 비교

초기 구조1차 개선2차 개선최종 개선
md → html 변환 스크립트 전체 실행시간(없음)5.5s3.9s4s
하나의 게시글을 가져오는 로직27ms0.2ms0.2ms0.2ms
모든 게시글을 가져오는 로직755ms22.5ms16.8ms18.5ms
/blog 전체 목록 페이지761ms24.2ms18.6ms20.3ms
상세보기 버튼 눌렀을 때27ms0ms90.2ms87.1ms
/blog/{카테고리}/{게시글} 이동13ms4.8ms9.9ms5.4ms

정리

  • 런타임 Markdown 변환은 글이 쌓일수록 부담이 커지는 구조였다.
  • 빌드 타임 변환으로 런타임 병목은 제거했지만, 데이터 덩어리가 무거우면 다른 비효율이 생겼다.
  • HTML과 메타를 분리하면서 “목록/상세의 조회 패턴”을 구조에 반영했다.
  • 마지막에는 메타데이터까지 카테고리 단위로 쪼개서, 성장할수록 비용이 커지는 문제를 해결했다.