블로그, 널리고 널렸는데 왜 직접 만들었나 🙄
들어가며 블로그 플랫폼은 이미 널리고 널렸습니다. Velog, Medium, Notion, 티스토리, 거기에 Hugo/Astro 같은 정적 사이트 생성기까지. 그런데도 굳이 힘들게 Nextjs로 블로그를 또 만들었습니다. 이유는 의외로 단순합니다.
들어가며
블로그 플랫폼은 이미 널리고 널렸습니다. Velog, Medium, Notion, 티스토리, 거기에 Hugo/Astro 같은 정적 사이트 생성기까지. 그런데도 굳이 힘들게 Nextjs로 블로그를 또 만들었습니다. 이유는 의외로 단순합니다.
블로그는 읽기는 엄청 많고, 쓰기는 거의 없는서비스거든요, 이 한 문장이 거의 모든 기술 결정의 출발점 입니다. 글 하나를 발행하면 그 뒤로 수천 번 읽히는 동안 내용은 안 바뀌잖아요. 그렇다면 "요청 들어올 때마다 DB에서 글 꺼내고, HTML 만드는" 흔한 웹앱 구조는 그냥 낭비라고 생각했습니다.
이 시리즈는 그 낭비를 어떻게 걷어내는지, 그 과정에서 Nextjs와 Supabase 를 어떻게 활용했는지에 대한 기록입니다.
한 줄로 보는 흐름
전체 데이터 흐름은 이렇게 흘러갑니다.
글 저장(Mardown) -> Supabase Postgres
-> 빌드 타임에 전부 정적 HTML로 구워내기 (SSG)
-> Nextjs가 /posts/[slug] 같은 모든 페이지를 프리렌더 처리
-> 글 발행할 때만 Webhook 한번, 그리고 /api/revalidate
-> 바뀐 페이지만 콕 집어서 다시 굽기 (Tag 기반 ISR)
핵심은 일단 전부 미리 굽고, 바뀐 데만 다시 도려내서 다시 굽는다 입니다. 사용자가 글을 읽을 땐 DB도, 렌더링도 거치지 않습니다. 이미 만들어둔 HTML이 그냥 나갈 뿐입니다.
기술 스택이랑 고른 이유
- Nextjs 16 (App Router) - 빌드타임 프리렌더 + 페이지 단위 갱신(ISR)을 한 프레임워크에서 다 해결합니다.
- Supabase (Postgres) - 글은 정형 데이터입니다. 파일(MDX)보다 관계형 DB가 맞습니다.
- Typescript* - 글,태그,시리즈 사이의 관계를 타입으로 못박아 둡니다.*
- Tailwind v4 - 디자인 시스템을 토큰으로 들고 가기 편해요.
- unified (remark/rehype) + Shiki - 마크다운 -> HTML 변환을 빌드 타임에 끝냅니다.
- BlockNote - 노션처럼 생긴 블록 에디터로 글을 작성합니다.
이 중에 두개는 좀더 짚고 넘어 가겠습니다.
1. 글을 파일이 아니라 DB에 저장합니다. 개발자 블로그 보면 '.mdx' 파일을 git에 커밋하는 방식을 많이 사용합니다. 간단해서 좋은데 글이 쌓이고 태그와 시리즈, 카테고리, 조회수 처럼 관계랑 상태가 붙기 시작하면 파일 구조가 금방 버거워 집니다. masonlab은 글을 Postgres에 Mardown 텍스트로 저장하고, 태그/시리즈/저자를 관계 테이블로 묶었습니다. 글쓰기는 RLS(Row Level Security)로 관리자만 작성핤 있도록 막아뒀습니다.
2. 그래도 읽기는 100% 정적이에요. DB에 저장한다고 매 요청마다 DB를 떄리는게 싫었습니다. 빌드 시점에 발행된 글들의 slug를 전부 긁어와서 정적 페이지로 미리 구워둡니다.
// scr/app/(site)/posts/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPublishedSlugs();
return posts.map((p) => ({ slug: p.slug }); // 발행된 모든 글을 빌드 떄 미리 굽기.
}DB는 빌드할 때랑 글 발행할 떄만 등장합니다.
이 블로그의 심장은 "외과적 갱신"
글 하나를 발행했다고 사이트 전체를 다시 빌드하면, 글이 500개일떄 1개 고치자고 500개를 다시 굽는 꼴이 됩니다. masonlab은 그렇게 하지 않고 supabase가 posts 테이블 변경을 감지해서 webhook 으로 /api/revalidate를 호출 하면 Nextjs가 태그 기반으로 영향받은 캐시만 무효화 합니다.
// src/app/api/revalidate/route.ts
revalidateTag(CACHE_TAGS.posts, "max"); // 글 관련 캐시 한방에 무효화
revalidatePath("/") // 홈, 목록, 사이트맵, RSS 도 살짝 건드린 후
revalidatePath("/posts");
if (slug) revlidatePath(`/posts/${slug}`); // 바뀐 글 페이지만 콕 집어서revalidateTag 한 번이면 그 태그 붙은 페이지(홈,목록,카테고리,태그별 페이지)가 다음 방문 때 새로 구워집니다. 전체 재빌드 없이, 발행하자마자, 필요한 곳만요. 여기에 안전장치로 시간 기반 폴백도 둬서, 혹시 웹훅이 빠져도 결국엔 최신으로 맞춰집니다.
// 같은 페이지 상단 - 1시간마다 자동 갱신되는 안전장치
export const revalidate = 3600;참고로 Nextjs 16의 새
cacheComponents(PPR) 대신, 검증된generateStaticParams + revalidate + revalidateTag조합을 일부러 골랐습니다. DB 기반 블로그를 빌드 타임에 완전히 프리렌더하는 가장 튼튼한 길이거든요. PPR로 갈아타는 건 언제든 열여뒀습니다.
본문 렌더링도 미리 끝내놔요
글 본문은 Markdown으로 저장되는데, 이걸 HTML로 바꾸고 코드 하이라이팅까지 빌드 타임에 처리해요. unified 파이프라인이 GFM(표·체크박스), 헤딩 앵커(목차/딥링크용), Shiki 기반 문법 강조를 한 번에 처리하거든요.
// src/lib/markdown.ts (발췌)
const processor = unified()
.use(remarkParse)
.use(remarkGfm) // 표, 체크박스, 자동 링크
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
.use(rehypeSlug) // 헤딩에 id 부여
.use(rehypeAutolinkHeadings, { behavior: "wrap" })
.use(rehypePrettyCode, { theme: "github-dark" }) // Shiki 빌드타임 하이라이팅
.use(rehypeStringify);여기서 포인트는 코드 블록 하이라이팅에 클라이언트 자바스크립트가 1바이트도 안 든다는 거예요. 흔히 쓰는 highlight.js나 Prism은 브라우저에서 색을 칠하는데, Shiki는 빌드 때 색칠을 다 끝낸 HTML을 내보내요. 읽는 사람 입장에선 그냥 색 입혀진 코드가 바로 보일 뿐이죠.
이 시리즈에서 다룰 것들
1편은 큰 그림이었어요. 다음 편들에선 각 결정을 코드 레벨로 파고들어 보겠습니다.
-
#2 — 읽기는 정적으로, 발행은 즉시: SSG + ISR + 태그 기반 캐싱의 실제
unstable_cache, 캐시 태그 설계, 웹훅 인증, 시간 폴백까지. "전부 굽고 바뀐 데만 도려내기"를 코드로 보여드릴게요. -
#3 — Markdown 한 줄이 화면이 되기까지: unified 파이프라인 해부
remark → rehype 변환, Shiki 빌드타임 하이라이팅, 이미지 캡션을<figure>로 바꾸는 커스텀 플러그인, 목차·읽기 시간 뽑아내기. -
#4 — 검색 엔진이랑 사람 둘 다 챙기기: SEO와 글쓰기 경험
generateMetadata, JSON-LD(BlogPosting·BreadcrumbList), OpenGraph 이미지, RSS, 그리고 BlockNote 에디터로 글 쓰는 관리자 경험까지.
코드와 비즈니스, 그 사이에서 배운 것들을 기록하고 공유합니다.