바이브코딩
DB 런타임 발행 | revalidatePath와 라이브 smoke
200은 무효화 신호일 뿐, 공개 화면은 마커로 확인한다
DB 런타임 발행 루프는 Next.js App Router에 이미 배포된 렌더러를 두고, Turso 같은 DB의 글 row만 바꾼 뒤 revalidatePath와 revalidateTag로 캐시 무효화 신호를 보내는 발행 방식이다. 전제는 Next.js 14 이상 App Router와 안정된 마크다운 렌더러다. Pages Router나 다른 DB면 API와 스크립트 경로가 달라진다.
일반적인 Vercel 배포는 코드, 의존성, 환경 변수를 함께 다시 컴파일한다. 작은 문구 수정도 전체 릴리스가 된다. DB-only 발행은 그 빌드를 건너뛴다. 대신 Git 히스토리, PR 리뷰, 롤백 버튼처럼 배포 파이프라인에 딸려 있던 안전장치도 같이 사라진다. 초안 작성(사람 또는 에이전트)과 운영 공개(검증, DB, 캐시, smoke)를 분리하지 않으면, 초안 품질 문제가 곧바로 라이브 사고로 이어진다.
가장 자주 깨지는 가정은 “revalidate가 됐으면 화면도 즉시 바뀐다”이다. revalidate는 재생성 완료를 보장하지 않고, 무효화 신호만 전달한다. 200 응답은 “무효화 표시가 전달됨”이지 “즉시 재생성이 끝남”이 아니다. stale-while-revalidate 구간이나 캐시 소유권(임베디드 리플리카, 인스턴스, CDN)이 다르면, DB에는 새 내용이 있어도 화면은 옛 내용이 잠깐 고정될 수 있다. 성공 기준은 curl 상태 코드가 아니라 라이브 URL에서 새 제목 마커가 보이는지다.
ISR은 s-maxage와 stale-while-revalidate를 응답한다. 앞단 CDN이 HTML을 붙잡고 있으면 Next.js 내부 무효화만으로는 화면이 안 바뀐다. revalidate 직후 해당 URL만 골라 purge하고 smoke를 다시 돈다. 첫 방문이 옛 화면인 것 자체만으로 실패로 단정하지 않는다. smoke의 2~5회, 2초 간격은 그 stale 구간을 넘기기 위한 것이다.
코드 배포 vs row 변경
작업 시작 전에 사람에게도 에이전트에게도 먼저 보여주는 표다. FAQ JSON 필드 값 추가는 렌더러가 이미 표시하도록 배포돼 있으면 DB 루프다. FAQ UI 컴포넌트나 스키마 검증 로직을 바꾸면 코드 배포다.
| 작업 | DB 루프 | 코드 배포 |
|---|---|---|
| 제목, 본문, 태그, 발행일 | 가능 | |
| FAQ 같은 JSON 필드 값 추가 | 가능 | |
| 오탈자, 출처 보강 | 가능 | |
| Markdown 렌더러 문법 추가 | 필요 | |
generateMetadata 로직 변경 | 필요 | |
| 카드 UI, 레이아웃 | 필요 | |
| 새 카테고리(라우팅, 메뉴) | 혼합 | 라우팅은 코드 |
| sitemap 생성 로직 | 필요 |
이 표의 빈칸은 금지 표시가 아니라 이 루프의 대상이 아니라는 뜻이다. 라우팅과 메뉴가 필요한 새 카테고리는 데이터 행만으로는 목록에 안 뜨는 경우가 있다. sitemap 생성 로직이 빌드 산출물이면 글 row를 바꿔도 XML이 안 따라온다.
generateMetadata가 DB를 읽도록 이미 배포돼 있으면, revalidate 후 다음 요청부터 SEO 메타가 따라온다. 메타 생성 로직 자체를 바꾸려면 코드 배포가 필요하다. 상세 path와 목록 tag를 같이 무효화한다.
렌더러가 이미 안정적일 때만 이 루프가 성립한다. 표가 깨지거나 코드블록이 안 나오면 JSON이 아니라 컴포넌트를 고친다. 에이전트에게 “이 글 고쳐 달라”만 던지면 renderer까지 PR이 나올 수 있다. 프롬프트에 content JSON만 다룬다는 문장을 고정한다.
로컬 JSON을 Git에 두면 초안 이력은 남고, Turso row는 공개 상태만 가진다. 배포 파이프라인의 revert 버튼이 없으므로, 사고 복구의 좌표는 이전 JSON을 다시 올리거나 status를 draft로 되돌리는 쪽이다. Draft Mode는 미리보기 렌더용이며, 공개 성공 기준을 대체하지 않는다.
발행 경로는 세 칸이다. 왼쪽은 사람이 쓰는 로컬 JSON 원본과 Git 보관, 가운데는 시스템이 쓰는 Turso row와 revalidate API, 오른쪽은 독자가 보는 캐시된 Next.js 페이지와 live smoke다. 코드 레인은 그대로 두고 가운데만 움직이는 것이 핵심이다.
[로컬 원천] [DB 런타임] [공개 화면]
local JSON -> Turso row -> Next.js page
^ | |
| v v
[ Git 보관 ] [ revalidate API ] [ live smoke ]
위 그림의 화살표는 데이터가 흐르는 방향이다. Git은 원본을 보관하고, revalidate API는 가운데에서 오른쪽으로 무효화 신호를 보낸다. smoke는 오른쪽 화면이 새 마커를 보여 주는지 읽는다.

revalidatePath
revalidatePath는 특정 경로의 캐시를 무효화 표시한다. Server Action과 Route Handler에서 호출한다. 즉시 재생성이 아니다. stale-while-revalidate 구간에서는 첫 smoke 요청이 옛 화면을 받을 수 있고, 그 자체만으로 실패로 단정할 수 없다.
인자 규칙은 셋이다. 리터럴 경로(/vibe-coding/my-post)를 넘길 때는 type을 생략한다. 그 한 페이지만 무효화된다. 패턴(/vibe-coding/[slug])을 넘길 때는 type: 'page'가 필수다. 매칭되는 전부가 무효화된다. /page나 /layout을 문자열 끝에 붙이지 않는다. 그것은 type으로 표현한다.
리터럴과 패턴을 섞어 쓰면 의도와 다른 범위가 무효화된다. 상세 한 글만 고쳤는데 카테고리 전체가 다시 그려지거나, 반대로 목록만 남고 상세가 옛 HTML인 상태가 그렇게 나온다. Next.js revalidatePath 문서가 이 구간의 1차 출처다.
revalidateTag
revalidateTag는 같은 태그를 달고 fetch한 페이지를 한꺼번에 무효화한다. 목록, 홈, sitemap처럼 여러 페이지가 공유하는 데이터에 쓴다. Server Action과 Route Handler에서 호출하고, 즉시 갱신은 아니다.
updateTag만 Server Action 전용이며, 다음 요청이 새 데이터를 기다린다. read-your-own-writes가 필요할 때 등장한다. 공개 발행 루프의 기본은 revalidate 쪽이다. 저장 직후 같은 세션에서 미리보기를 보여 줘야 하면 updateTag를 본다.
태그를 안 붙인 페이지는 revalidateTag가 지나간다. posts:category 태그와 posts:home 태그는 목록 컴포넌트가 같은 태그를 달고 fetch할 때만 의미가 있다. 상세는 경로 무효화, 목록은 태그 무효화로 나누는 구성이 흔하다. 한쪽만 보내면 목록만 새데 상세가 옛것이거나, 그 반대가 된다.
| 함수 | 어디서 호출 가능 | 즉시 갱신 | 용도 |
|---|---|---|---|
revalidatePath(path) | Server Action, Route Handler | 아니오 | 특정 페이지 |
revalidateTag(tag) | Server Action, Route Handler | 아니오 | 목록, 홈, sitemap처럼 여러 페이지가 공유하는 데이터 |
updateTag(tag) | Server Action 전용 | 예, 다음 요청이 기다림 | read-your-own-writes |
핵심 포인트: 200은 무효화 신호가 전달됐다는 뜻이다. 공개 화면의 성공 기준은 상세 URL과 목록 URL에서 새 제목 마커가 보이는지다.
revalidatePath에 패턴 /vibe-coding/[slug]를 넘길 때는 type: 'page'가 필수다. 리터럴 /vibe-coding/my-post에는 type을 생략한다. 경로 끝에 /page나 /layout을 붙이지 않는다. 그것은 type으로 표현한다. 리터럴과 패턴을 섞어 쓰면 의도와 다른 범위가 무효화된다. 상세 한 글만 고쳤는데 카테고리 전체가 다시 그려지거나, 반대로 목록만 남고 상세가 옛 HTML인 상태가 그렇게 나온다. updateTag는 Server Action 전용이다. 다음 요청이 새 데이터를 기다린다. Route Handler에서 updateTag를 쓰면 문서가 가리키는 호출 위치가 아니다.
엔드포인트 시크릿
App Router 기준 예시는 app/api/revalidate/route.ts에 둔다. URL은 공개되어도 x-revalidate-auth 같은 시크릿 헤더 검증은 필수다. 검증 없이 열어 두면 누구나 캐시를 깨 DoS에 가까운 부하를 줄 수 있다. publish 스크립트와 CI에만 시크릿을 둔다. REVALIDATE_SECRET에 NEXT_PUBLIC_ 접두사를 붙이지 않는다. 붙는 순간 빌드 결과물에 노출된다.
타이밍 안전 비교는 문자열을 ===로 비교하지 않는다. 길이가 다르면 바로 false이고, 같으면 timingSafeEqual로 바이트를 비교한다. 경로 주입을 막기 위해 slug와 category는 소문자와 하이픈만 받는다. 핸들러는 다음 블록이다.
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
import { timingSafeEqual } from 'node:crypto';
export const runtime = 'nodejs';
function safeEqual(given: string, secret: string) {
const a = Buffer.from(given, 'utf8');
const b = Buffer.from(secret, 'utf8');
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}
export async function POST(req: NextRequest) {
const secret = process.env.REVALIDATE_SECRET;
const given = req.headers.get('x-revalidate-auth');
if (!secret || !given || !safeEqual(given, secret)) {
return NextResponse.json({ ok: false, error: 'unauthorized' }, { status: 401 });
}
let body: { slug?: string; category?: string };
try {
body = await req.json();
} catch {
return NextResponse.json({ ok: false, error: 'invalid json' }, { status: 400 });
}
const { slug, category } = body;
if (!slug || !category) {
return NextResponse.json({ ok: false, error: 'missing slug or category' }, { status: 400 });
}
if (!/^[a-z0-9-]+$/.test(slug) || !/^[a-z0-9-]+$/.test(category)) {
return NextResponse.json({ ok: false, error: 'invalid slug or category' }, { status: 400 });
}
const paths = [`/${category}/${slug}`, `/${category}`, '/'];
try {
for (const p of paths) revalidatePath(p);
revalidateTag(`posts:${category}`);
revalidateTag('posts:home');
revalidatePath('/sitemap.xml');
} catch (err) {
return NextResponse.json(
{ ok: false, error: 'revalidate failed', detail: String(err) },
{ status: 500 },
);
}
return NextResponse.json({
ok: true,
marked: paths,
note: 'invalidation applies on next visit; verify with a live fetch',
});
}
응답 JSON의 note가 말하듯, 무효화는 다음 방문에 적용된다. 경로 배열은 상세와 카테고리 목록과 홈을 한 번에 무효화하기 위한 것이다. sitemap.xml은 경로 무효화로 따로 넣는다.

smoke 마커
읽을 값은 두 가지다. 본문에 새 제목 마커가 있는지, 캐시 헤더가 stale 구간을 통과했는지. revalidateTag는 기본적으로 stale-while-revalidate 구간을 남기기 때문에, 첫 방문이 옛 화면을 주는 것은 정상 동작일 수 있다. 2~5회, 2초 간격으로 같은 URL을 다시 받는 스크립트 예시는 다음 블록이다.
#!/usr/bin/env bash
set -euo pipefail
URL="$1"
MARKER="$2"
for i in 1 2 3 4 5; do
BODY="$(curl -sS --compressed -H 'Cache-Control: no-cache' "$URL")"
if echo "$BODY" | grep -qF "$MARKER"; then
echo "OK $URL (attempt $i)"
exit 0
fi
echo ".. $URL not yet (attempt $i)"
sleep 2
done
echo "FAIL $URL marker not found: $MARKER" >&2
exit 1
$1은 라이브 URL, $2는 본문에 있어야 할 새 제목 문자열이다. -qF는 고정 문자열 검색이라 제목의 정규식 특수문자가 패턴으로 해석되지 않는다. --compressed는 gzip 본문을 풀어 마커를 찾게 한다. Cache-Control: no-cache는 로컬 curl 캐시가 아니라, 중간에 있는 캐시에 재검증을 요청하는 헤더다. 그래도 앞단 CDN이 무시하면 퍼지가 따로 필요하다.
검증은 상세와 목록 양쪽이다. 호출 예시는 다음 블록이다.
./scripts/smoke.sh "https://example.com/vibe-coding/my-post" "새 제목 문자열"
./scripts/smoke.sh "https://example.com/vibe-coding" "새 제목 문자열"
목록만 새데 상세가 옛것이거나, 그 반대면 태그 무효화와 경로 무효화 중 한쪽이 빠진 것이다.
CDN 리플리카
모든 단계가 성공처럼 보여도 화면만 옛날인 경우는 보통 세 가지다.
Turso embedded replicas를 쓰면 쓰기는 원격으로 가고, 읽기는 로컬 복제본에서 온다. 복제본은 syncInterval 주기로 따라잡는다. 발행 루프는 upsert 후 곧바로 revalidate를 보내고, 다음 방문 때 렌더러가 로컬 복제본을 읽는다. 결과적으로 캐시는 “신선한 것처럼” 굳고, 다음 동기화가 끝나도 저절로 갱신되지 않는다. 다음 발행 때까지 옛 글이 걸려 있는 현상이 나온다. 발행 스크립트가 revalidate 전에, 재생성 요청이 읽는 경로에서 새 값이 보이는지를 확인한다. 쓰기 경로와 읽기 경로가 다르면 항상 생기는 현상이다.
기본 Next.js 캐시는 인스턴스별로 로컬에 붙는다. revalidateTag는 호출을 받은 인스턴스에서만 무효화된다. 로드밸런서 뒤에서 요청이 분산되면 새 글과 옛 글이 번갈아 보일 수 있다. 자체 호스팅에서 인스턴스가 여러 개라면 shared cache를 쓰거나, 커스텀 캐시 핸들러로 refreshTags 같은 동기화를 붙인다.
ISR은 s-maxage와 stale-while-revalidate를 응답한다. 앞단 CDN이 이 지시자를 존중해 HTML을 캐시하고 있다면, Next.js 내부 캐시 무효화만으로는 HTML이 바로 바뀌지 않는다. CDN이 앞단에 있으면 purge가 발행 루프의 필수 단계다. revalidate 직후 해당 URL만 골라 purge하고 smoke를 다시 돈다. Cloudflare Cache Purge 문서가 이 구간의 1차 출처다.

8단계
로컬 JSON 작성과 스키마 검증, 사람 품질 검토, DB upsert, 읽기 경로 확인, revalidate와 CDN purge, live smoke, 마커 미도달 시 2~5회 재시도가 한 루프다.
| 단계 | 작업 | 통과 기준 |
|---|---|---|
| 1 | 로컬 JSON 작성 | slug, category, title, content 존재 |
| 2 | 스키마 검증 | 필수 필드, 위험 HTML 없음 |
| 3 | 품질 검토(사람) | 얕은 요약 아님, 출처 보강 |
| 4 | DB upsert | id와 slug 일치, 영향 행 수 1 |
| 5 | 읽기 경로 확인 | 렌더러가 새 값을 읽는지 |
| 6 | revalidate와 CDN purge | 상세, 목록, 홈, sitemap |
| 7 | live smoke | 상세 URL 마커, 목록 URL 마커 |
| 8 | 재시도(선택) | marker 미도달이면 2~5회 |
완료 기준은 curl 성공이 아니라 라이브 URL에서 새 제목이 보이는지다. upsert가 성공했는데 사이트가 옛 글이면, Next.js와 CDN이 이전 HTML을 캐시하고 있는 경우가 많다. 5단계가 빠지면 리플리카가 옛 row를 읽어도 6단계가 “성공”으로 끝난다.
에이전트를 작성자가 아니라 운영 절차를 실행하는 도구로 둘 때의 경계 문장은 다섯이다. content/vibe/<slug>.json에만 파일을 작성한다. validation과 금지어 grep을 통과할 때까지 고친다. publish 스크립트는 승인 후에만 실행한다. 상세 URL과 목록 URL 양쪽에 live smoke를 돈다. 실패 시 보류 보고하고 다음 수정으로 넘어가지 않는다. 금지 범위는 app/, components/, lib/ 이하 파일 수정과 DB 직접 접속이다. 불확실하면 진행 중지와 근거 보고다.
작업 전에는 데이터 변경인지 코드 변경인지 구분하고, 로컬 JSON 원본과 되돌리기 방법이 있는지 본다. 검증 중에는 스키마, 금지어 grep, slug와 id 일치, 본문 실행 가치를 본다. 공개 후에는 revalidate 경로(상세, 목록, 홈, sitemap), live smoke(상세와 목록), 콘솔 오류를 같이 본다.
DB 되돌리기
DB만 바꾸면 Git 히스토리에 흔적이 없다. Git revert로는 발행을 취소할 수 없다. 응급 조치는 상태 토글이다. 공개 화면에서 내리는 SQL은 다음 블록이다.
UPDATE posts SET status = 'draft', published_at = NULL WHERE slug = 'problem-slug';
problem-slug는 사고 난 글의 slug다. status='draft'로 바꾸면 공개 목록 쿼리가 그 행을 빼는 전제다. published_at을 비우는 이유는 예약 발행이나 날짜 정렬이 옛 공개 시각을 붙잡고 있을 수 있어서다. 이후 revalidate를 호출하면 공개 화면에서 사라진다. 글 row는 DB에 남아 조사 후 재공개할 수 있다.
되돌린 뒤에는 상세만 내리지 말고 목록과 sitemap도 함께 무효화한다. 그렇지 않으면 죽은 링크가 남는다. content 폴더를 Git으로 관리하면 복구 기준점이 생긴다. 로컬 JSON 이전 버전을 재업로드한 뒤 revalidate하는 경로도 같다.
페이지 HTML의 og:image까지 런타임에 바뀌는 구조는 동적 OG 이미지와 같이 본다.
태그를 안 붙인 페이지
posts:category 태그와 posts:home 태그는 목록 컴포넌트가 같은 태그를 달고 fetch할 때만 의미가 있다. fetch 호출에 태그가 없으면 revalidateTag는 그 페이지를 지나간다. 상세는 revalidatePath로 리터럴 경로를 보내고, 목록과 홈은 태그로 보내는 구성이 흔하다. 한쪽만 보내면 목록만 새데 상세가 옛것이거나, 그 반대가 된다. sitemap.xml은 태그가 아니라 경로 무효화로 따로 넣는다. sitemap 생성 로직이 빌드 산출물이면 글 row를 바꿔도 XML이 안 따라온다. 그때는 코드 배포다.
generateMetadata가 DB를 읽도록 이미 배포돼 있으면, revalidate 후 다음 요청부터 SEO 메타가 따라온다. 메타 생성 로직 자체를 바꾸려면 코드 배포가 필요하다. FAQ JSON 필드 값 추가는 렌더러가 이미 표시하도록 배포돼 있으면 DB 루프다. FAQ UI 컴포넌트나 스키마 검증 로직을 바꾸면 코드 배포다. 새 카테고리는 데이터 행만으로는 목록에 안 뜨는 경우가 있다. 라우팅과 메뉴는 코드다.
로컬 JSON을 Git에 두면 초안 이력은 남고, Turso row는 공개 상태만 가진다. 가운데 레인이 빨라질수록 왼쪽 원본과 오른쪽 smoke가 없으면 무엇이 나갔는지 재구성하기 어렵다. Draft Mode는 미리보기 렌더용이며, 공개 성공 기준을 대체하지 않는다. 미리보기에서 새 제목이 보여도, 라이브 URL의 마커가 없으면 발행이 끝나지 않은 것이다.
revalidatePath에 패턴 /vibe-coding/[slug]를 넘길 때는 type: 'page'가 필수다. 리터럴 /vibe-coding/my-post에는 type을 생략한다. 경로 끝에 /page나 /layout을 붙이지 않는다. 그것은 type으로 표현한다. 리터럴과 패턴을 섞어 쓰면 의도와 다른 범위가 무효화된다. 상세 한 글만 고쳤는데 카테고리 전체가 다시 그려지거나, 반대로 목록만 남고 상세가 옛 HTML인 상태가 그렇게 나온다.
updateTag는 Server Action 전용이다. 다음 요청이 새 데이터를 기다린다. read-your-own-writes가 필요할 때 등장한다. 공개 발행 루프의 기본은 revalidate 쪽이다. 저장 직후 같은 세션에서 미리보기를 보여 줘야 하면 updateTag를 본다. Route Handler에서 updateTag를 쓰면 문서가 가리키는 호출 위치가 아니다.
시크릿 헤더 x-revalidate-auth는 publish 스크립트와 CI에만 둔다. URL이 공개되어도 헤더가 없으면 401이다. timingSafeEqual은 길이가 다를 때 바로 false이고, 같으면 바이트를 비교한다. ===로 문자열을 비교하면 타이밍으로 길이를 짐작할 여지가 생긴다. REVALIDATE_SECRET에 NEXT_PUBLIC_를 붙이면 빌드 결과물에 노출된다. slug와 category의 정규식 ^[a-z0-9-]+$는 경로 주입을 막는다. JSON이 아니면 400이다. revalidate 호출이 던지면 500이다.
Turso embedded replicas는 쓰기는 원격, 읽기는 로컬 복제본이다. syncInterval 주기로 따라잡는다. upsert 직후 revalidate를 보내면, 다음 방문의 렌더러가 옛 복제본을 읽고 그 결과를 캐시에 굳힌다. 다음 동기화가 끝나도 캐시는 저절로 갱신되지 않는다. 발행 스크립트가 revalidate 전에, 재생성 요청이 읽는 경로에서 새 값이 보이는지를 확인한다. 기본 Next.js 캐시는 인스턴스별로 로컬에 붙는다. revalidateTag는 호출을 받은 인스턴스에서만 무효화된다. 로드밸런서 뒤에서 새 글과 옛 글이 번갈아 보이면 shared cache나 커스텀 캐시 핸들러의 refreshTags가 대상이다.
ISR은 s-maxage와 stale-while-revalidate를 응답한다. 앞단 CDN이 HTML을 붙잡고 있으면 Next.js 내부 무효화만으로는 화면이 안 바뀐다. revalidate 직후 해당 URL만 골라 purge하고 smoke를 다시 돈다. Cloudflare Cache Purge 문서가 이 구간의 1차 출처다. smoke의 2~5회, 2초 간격은 그 stale 구간을 넘기기 위한 것이다. 첫 방문이 옛 화면인 것 자체만으로 실패로 단정하지 않는다.
에이전트 경계 다섯은 content/vibe/<slug>.json에만 파일을 작성하고, validation과 금지어 grep을 통과할 때까지 고치고, publish 스크립트는 승인 후에만 실행하고, 상세와 목록 양쪽에 smoke를 돌리고, 실패 시 보류 보고하는 것이다. app/, components/, lib/ 수정과 DB 직접 접속은 금지다. 표가 깨지거나 코드블록이 안 나오면 JSON이 아니라 컴포넌트를 고친다. 그 순간은 코드 배포다.
smoke 스크립트가 읽는 것
$1은 라이브 URL, $2는 본문에 있어야 할 새 제목 문자열이다. 루프는 1부터 5까지다. 매 시도마다 curl -sS --compressed -H 'Cache-Control: no-cache'로 본문을 받는다. -qF는 고정 문자열 검색이라 제목의 정규식 특수문자가 패턴으로 해석되지 않는다. 마커가 있으면 OK와 시도 횟수를 찍고 0으로 끝난다. 없으면 2초를 자고 다시 받는다. 다섯 번 모두 없으면 FAIL을 stderr에 찍고 1로 끝난다.
상세 URL과 목록 URL을 같은 마커로 두 번 호출한다. 목록만 새데 상세가 옛것이면 경로 무효화가 빠진 것이다. 반대면 태그가 빠진 것이다. 200만 보고 끝내면 stale-while-revalidate 구간의 옛 HTML을 성공으로 읽는다. 응답 JSON의 note가 말하듯 무효화는 다음 방문에 적용된다.
8단계 표의 통과 기준은 slug, category, title, content 존재, 필수 필드와 위험 HTML 없음, 얕은 요약이 아닌 출처 보강, id와 slug 일치와 영향 행 수 1, 렌더러가 새 값을 읽는지, 상세 목록 홈 sitemap 무효화, 상세와 목록 마커, 미도달이면 2~5회다. 4단계 upsert가 성공하고 5단계 읽기 경로가 옛 복제본이면 6단계 revalidate는 옛 값을 캐시에 굳힌다. 그래서 5단계가 빠지면 화면만 옛날인 사고가 난다.
되돌리기 SQL은 status = 'draft'와 published_at = NULL이다. 공개 목록 쿼리가 draft를 빼는 전제다. 날짜 정렬이 옛 공개 시각을 붙잡고 있을 수 있어 published_at을 비운다. 이후 revalidate를 호출해야 공개 화면에서 사라진다. 상세만 내리고 목록과 sitemap을 안 보내면 죽은 링크가 남는다. Git revert로는 발행을 취소할 수 없다. content 폴더의 이전 JSON을 재업로드한 뒤 revalidate하는 경로도 같다. problem-slug는 사고 난 글의 slug다.
엔드포인트 경로 배열은 /${category}/${slug}, /${category}, /다. 태그는 posts:${category}와 posts:home이다. sitemap.xml은 경로로 따로 넣는다. runtime은 nodejs다. 시크릿이 없거나 헤더가 없거나 safeEqual이 false면 401이다. invalid json과 missing slug, invalid slug는 400이다. revalidate가 던지면 500이다.
마무리
앞에서 다룬 DB 런타임 발행 루프의 핵심만 짧게 정리한다.
- 렌더러가 안정된 뒤에만 데이터 레인을 움직인다. 문법과 레이아웃은 코드 배포다.
revalidatePath와revalidateTag는 즉시 재생성이 아니다.updateTag만 Server Action에서 다음 요청을 기다린다.- 리터럴 경로는 type 생략, 패턴은
type: 'page'가 필수다. 경로 끝에/page를 붙이지 않는다. - 엔드포인트는 시크릿 헤더와 timingSafeEqual이 있고,
NEXT_PUBLIC_시크릿은 빌드에 새어 나간다. - smoke는 상세와 목록 양쪽, stale 구간은 2~5회 재시도다.
- 임베디드 리플리카, 인스턴스 로컬 캐시, 앞단 CDN은 200과 무관하게 옛 HTML을 붙잡는다.
- DB 발행의 되돌리기는 Git revert가 아니라
status='draft'토글과 재업로드다.
「무효화 신호와 공개 화면은 같은 사건이 아니다」 upsert 성공과 revalidate 200은 중간 상태다. 라이브 URL의 마커가 보이기 전에는 발행이 끝나지 않은 것으로 본다.
출처와 링크
- Next.js revalidatePath: 경로별 무효화와 type 인자
- Next.js revalidateTag: 태그 일괄 무효화
- Next.js updateTag: read-your-own-writes
- Next.js Incremental Static Regeneration: ISR과 stale-while-revalidate
- Turso embedded replicas: 읽기 지연과 동기화
- Cloudflare Cache Purge: CDN 캐시 잔존 시
- 동적 OG 이미지: 런타임 제목과 og:image 버전 쿼리
조사 기준: 2026년 8월. Next.js 14+ App Router와 Turso CLI 기준이다. Draft Mode는 미리보기 렌더용이며, 이 루프의 공개 성공 기준을 대체하지 않는다.
FAQ
자주 묻는 질문
revalidate만으로 SEO 메타가 바로 바뀌는가?
메타 생성 함수가 글 row를 읽도록 이미 나가 있는지가 전제다. 그 전제면 캐시를 깨는 신호 다음의 요청에서 제목과 설명이 따라온다. 함수 본문을 고치는 일은 렌더러 레인이라 코드 배포가 필요하고, 상세와 목록을 한쪽만 깨면 메타가 엇갈린다.
DB upsert는 성공했는데 사이트는 옛 글인 이유는 무엇인가?
Next.js와 앞단 CDN이 이전 HTML을 붙잡고 있는 경우가 많다. upsert 직후 revalidate API를 호출하고, 라이브 URL에서 새 제목 마커가 보이는지가 완료 기준이다. Cloudflare를 쓰면 purge도 같은 루프에 들어간다.
Git revert로 발행을 취소할 수 있는가?
DB만 바꾼 발행은 Git 히스토리에 남지 않아 revert로는 안 된다. status를 draft로 토글하거나 로컬 JSON 이전 버전을 재업로드한 뒤 revalidate한다. content 폴더를 Git으로 관리하면 복구 기준점이 생긴다.
FAQ JSON 필드만 추가해도 코드 배포 없이 되는가?
값이 JSON 칸에만 들어가고 화면이 그 칸을 이미 그리면 데이터 레인이다. 컴포넌트나 스키마 검사가 없으면 공개 화면에 안 나타나거나 검증에서 막힌다. 작업 전에 렌더러가 그 필드를 읽는지부터 가른다.
revalidate API를 공개 URL에 두어도 되는가?
URL은 공개되어도 x-revalidate-auth 같은 시크릿 헤더 검증은 필수다. 검증 없이 열어 두면 누구나 캐시를 깨 부하를 줄 수 있다. 시크릿은 publish 스크립트와 CI에만 두고 NEXT_PUBLIC_ 접두사는 붙이지 않는다.
에이전트에게 글 수정을 맡길 때 빠지면 안 되는 경계 문장은 무엇인가?
app, components, lib 아래는 수정하지 않고 content JSON만 다룬다는 문장이다. validation, publish, smoke 순서와 실패 시 보류 보고를 같이 적으면 renderer PR 사고를 줄일 수 있다. 불확실하면 진행을 멈춘다.
용어
관련 용어
정적 파일이나 빌드 결과가 아니라 런타임 데이터베이스에 저장된 값을 프론트엔드가 읽어 화면을 구성하는 방식이다. VIBE 코딩에서는 AI가 만든 글, 앱 소개, 사전 항목을 매번 배포하지 않고 DB에 반영한 뒤 공개 페이지에서 읽게 만들 때 중요하다. 이 구조를 쓰면 콘텐츠 변경과 코드 배포를 분리할 수 있지만, 스키마 검증·캐시 갱신·공개 스모크 테스트를 함께 설계해야 한다.
학습·온보딩 스캐폴드프로젝트의 기본 구조(폴더 구조, 설정 파일, 보일러플레이트 코드, 환경 변수 템플릿)를 자동으로 생성하는 것으로, '비계(scaffold)'라는 건축 용어에서 유래했다. 건축에서 비계가 건물을 짓기 위한 임시 구조물인 것처럼, 코딩에서 스캐폴드는 프로젝트를 시작하기 위한 기본 틀을 제공한다. 바이브 코딩에서 스캐폴드는 가장 효과적인 첫 단계이다. AI에게 'Next.js + Tailwind + Supabase 프로젝트를 초기 설정해줘'라고 하면 전체 폴더 구조(app/, components/, lib/, hooks/), 설정 파일(next.config.js, tailwind.config.js, tsconfig.json), 환경 변수 템플릿(.env.example), 패키지 의존성(package.json), 기본 레이아웃 컴포넌트까지 한 번에 생성한다. 이후 이 스캐폴드 위에서 기능을 하나씩 추가해나가는 것이 바이브 코딩의 권장 워크플로이다. create-next-app, create-vite 같은 전통적 CLI 스캐폴더도 있지만, AI를 통한 스캐폴딩은 프로젝트의 구체적 요구사항(사용할 DB, 인증 방식, 디자인 시스템 등)을 반영한 맞춤형 초기 설정을 제공한다는 점에서 차별화된다.
브라우저·노코드 빌더 브이제로Vercel이 만든 AI UI 빌더로, 자연어 설명만으로 프로덕션 수준의 React + Tailwind CSS 컴포넌트를 즉시 생성한다. UI/프론트엔드에 특화된 도구로, 풀스택 개발보다는 '보이는 부분'을 빠르게 만드는 데 집중한다. 가장 특징적인 기능은 코드 생성 전 '계획 단계'를 거친다는 것으로, 어떤 페이지를 만들 것인지, 어떤 기능을 포함할 것인지, 어떤 기술을 사용할 것인지를 먼저 보여주고 사용자의 확인을 받은 후 코드를 생성한다. shadcn/ui 컴포넌트 라이브러리와 Tailwind CSS를 기본으로 사용하여, 생성된 코드가 일관되고 모던한 디자인을 유지한다. 생성된 컴포넌트를 Vercel에 원클릭으로 배포할 수 있으며, Next.js 프로젝트에 직접 복사하여 사용하는 것도 간편하다. 권장 워크플로는 'v0에서 UI 프로토타입 → Cursor에서 비즈니스 로직 구현'으로, UI 디자인에 시간을 쏟지 않고 빠르게 시각적 결과물을 확인하고 싶을 때 가장 효과적이다.
링크
관련 링크
쉬운 보안을 지향하는 한국어 보안 계정으로, AI·VIBE 코딩 흐름에서 놓치기 쉬운 보안 감각을 되짚는 데 유용합니다.
VIBE 코딩 제품 리서치 Killed by Google · Google GraveyardGoogle이 종료한 서비스와 제품을 한눈에 모아, 플랫폼 의존성과 제품 지속성 리스크를 판단하게 해 주는 ‘Google 묘지’ 아카이브입니다.
VIBE 코딩 레퍼런스 웹사이트 해부도 · Website Anatomy MapAI와 웹사이트를 함께 만들 때 ‘그 부분’이 아니라 정확한 UI·웹 용어로 지시할 수 있게 돕는 영-한 시각 사전입니다.
관련 글
관련 글
Recommended
Cloudflare R2 이그레스 | Hermes 홈과 파일 권한 분리
Cloudflare R2는 객체 저장과 두 종류의 연산으로 과금한다. 2026년 8월 Cloudflare 문서 기준으로 Standard 저장은 GB-월당 0.015달러, Class A는 100만 건당 4.50달러, Class B는 100만 건당 0.36달러이고, 인터넷으로 나가는 이그레스는 0달러다. 무료 티어는 매달 저장 10GB, Class A 100만, Class B 1,000만이다. 요금 페이지 갱신일은 2026-08-07이다.
이그레스가 무료인 것과 저장·연산이 무료인 것은 다르다. 문서 각주는 R2에서 직접 나가는 전송(Workers API, S3 API, r2.dev 포함)에는 데이터 전송 요금이 없다고 적으면서, **다른 종량제 서비스를 R2 버킷에 연결하면 그 서…
AI 영상 플랫폼 비교 | 빠나나 AI, ZCRE, Newtake, PAI 요금과 크레딧 단가, k…
AI 영상 플랫폼 4곳을 한 표에 놓고 비교했다. 대상은 국내 회사가 운영하는 빠나나 AI와 ZCRE(지크), 해외 회사가 운영하는 Newtake(뉴테이크)와 PAI(Utopai X)다. 넷 다 브라우저에서 이미지와 영상을 만들고, 매달 받는…