[blog - 9] Markdown 변환부터 메타데이터 분리까지 블로그 구조 개선 기록
게시글이 109개까지 늘어나면서 /blog 목록 페이지 로딩 속도가 761ms까지 늘었다.
원인은 페이지를 열 때마다 브라우저가 Markdown을 HTML로 바꾸고 있었기 때문이다.
문제 0 — 런타임 Markdown 변환이 누적 비용이 됐다
초기 구조는 페이지에서 렌더링(런타임)할 때마다 클라이언트가 Markdown → HTML 변환을 수행하는 구조였다.
/content/posts/{카테고리}/{slug}.md
측정 결과
- 총 Markdown 파일: 109개 (332KB)
/blog목록 페이지 로딩: 761ms- 단일 게시글 MD → HTML 변환: 27ms
- 글이 길수록 변환 비용이 커져서 체감 렉이 생겼다
개선 1 — 변환을 빌드 타임으로 옮겼다
첫 번째 선택은 “런타임 비용을 없애자”였다.
- 빌드 단계에서 Markdown → HTML 변환
- 결과를 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 파일
/content/build/posts/{카테고리}/{게시글}.html
메타데이터(하나로 합친 JSON)
/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 파싱
- 카테고리 필터링
지금은 괜찮더라도 좋지 않은 구조라고 생각했다.
최종 개선 — 메타데이터를 카테고리 단위로 분산했다
마지막 선택은 “필요한 만큼만 가져오게 하자”였다.
최종 메타데이터 구조
/content/build/meta/posts/{카테고리}/{slug}.json
HTML은 그대로 유지했다.
/content/build/posts/{카테고리}/{slug}.html
추가로 카테고리/시리즈는 별도 파일에서 관리했다.
category.jsonseries.json
측정 결과는 이렇다.
- 스크립트 전체 실행시간: 4s
- 모든 게시글 조회: 18.5ms
/blog목록 페이지: 20.3ms- 상세보기 버튼 요청: 87.1ms
- 단일 게시글 조회: 0.2ms
1~2차 개선처럼 드라마틱한 속토의 차이는 없었지만 확장성 측면에서 더 큰 이점이 생겼다.
- 카테고리 목록은 카테고리만 읽으면 된다
- 특정 카테고리 목록도 그 범위만 읽으면 된다
- 메타데이터의 기본 다운로드 단위가 작아졌다
성능 비교
| 초기 구조 | 1차 개선 | 2차 개선 | 최종 개선 | |
|---|---|---|---|---|
| md → html 변환 스크립트 전체 실행시간 | (없음) | 5.5s | 3.9s | 4s |
| 하나의 게시글을 가져오는 로직 | 27ms | 0.2ms | 0.2ms | 0.2ms |
| 모든 게시글을 가져오는 로직 | 755ms | 22.5ms | 16.8ms | 18.5ms |
/blog 전체 목록 페이지 | 761ms | 24.2ms | 18.6ms | 20.3ms |
| 상세보기 버튼 눌렀을 때 | 27ms | 0ms | 90.2ms | 87.1ms |
/blog/{카테고리}/{게시글} 이동 | 13ms | 4.8ms | 9.9ms | 5.4ms |
정리
- 런타임 Markdown 변환은 글이 쌓일수록 부담이 커지는 구조였다.
- 빌드 타임 변환으로 런타임 병목은 제거했지만, 데이터 덩어리가 무거우면 다른 비효율이 생겼다.
- HTML과 메타를 분리하면서 “목록/상세의 조회 패턴”을 구조에 반영했다.
- 마지막에는 메타데이터까지 카테고리 단위로 쪼개서, 성장할수록 비용이 커지는 문제를 해결했다.