심층 학습 가이드
DB만 바꾸고 발행하기 | revalidate가 실제로 하는 일과 안 하는 일
200이 떴는데 화면이 안 바뀌는 다섯 가지 이유
- 핵심 주제
- DB Runtime Publishing
- 예상 시간
- 18분
- 업데이트
- 2026.04.30
- 키워드
- DB 업로드 revalidate · 무배포 포스팅 · Next.js ISR
핵심 명제는 하나입니다. 초안 작성(AI + 사람)과 운영 공개(검증, DB, 캐시, smoke)를 분리한다는 것입니다. 이 분리를 하면 초안 품질 문제가 곧바로 라이브 사고로 이어지는 경로를 끊을 수 있습니다.
다만 DB-only 루프는 빠르기 때문에 채택하면 위험합니다. 배포 파이프라인이 사라지는 만큼, 거기 딸려 있던 안전장치도 같이 사라집니다. Git 히스토리, PR 리뷰, 롤백 버튼이 없습니다. 이 글의 절반은 그걸 운영 체크리스트와 검증 절차로 다시 만드는 이야기입니다.
또한 “revalidate가 됐으면 화면도 즉시 바뀐다”라는 가정이 가장 자주 깨집니다. revalidate는 재생성 완료를 보장하지 않고, 무효화 신호만 전달합니다. stale-while-revalidate 구간이나 캐시 소유권(리플리카, 인스턴스, CDN)이 다르면, DB에는 새 내용이 있어도 화면은 옛 내용이 잠깐 고정될 수 있습니다. 그래서 이 글은 200 응답을 통과 기준으로 삼지 않고, 라이브 URL smoke를 성공 기준으로 두는 방법까지 포함합니다.
한 줄 고치려고 빌드를 돌릴 필요는 없습니다
일반적인 Vercel 배포는 코드, 의존성, 환경 변수를 함께 다시 컴파일합니다. 작은 문구 수정도 전체 릴리스가 됩니다. 반면 렌더러는 이미 배포된 상태이고, DB-only 발행은 글 데이터만 바꾼 뒤 캐시 무효화 신호만 보냅니다.
여기서 핵심 명제는 “데이터 레인만 움직이는 발행 루프”입니다. ISR과 revalidatePath, revalidateTag의 역할을 정확히 이해한 뒤, 읽기 경로까지 같이 확인해야 “200은 성공, 화면은 갱신”이 성립합니다.
세 레인
[로컬 원천] [DB 런타임] [공개 화면]
local JSON ──▶ Turso row ──▶ Next.js page
▲ │ │
│ ▼ ▼
[ Git 보관 ] [ revalidate API ] [ live smoke ]
왼쪽은 사람이 쓰는 원본, 가운데는 시스템이 쓰는 저장소, 오른쪽은 독자가 보는 캐시된 페이지입니다. 코드 레인은 그대로 두고 가운데만 움직이는 것이 핵심입니다.
revalidate가 실제로 하는 일과 안 하는 일
여기서 대부분의 오해가 생깁니다. 정확히 정리하면 이렇습니다.
| 함수 | 어디서 호출 가능 | 즉시 갱신? | 언제 쓰나 |
|---|---|---|---|
revalidatePath(path) | Server Action, Route Handler | 아니오 | 특정 페이지 |
revalidateTag(tag) | Server Action, Route Handler | 아니오 | 목록, 홈, sitemap처럼 여러 페이지가 공유하는 데이터 |
updateTag(tag) | Server Action 전용 | 예, 다음 요청이 기다림 | read-your-own-writes |
revalidate는 빌드가 아닙니다. 200 응답은 “무효화 표시가 전달됨”이지, “즉시 재생성이 끝남”이 아닙니다. stale-while-revalidate 구간에서는 첫 smoke 요청이 옛 화면을 받을 수도 있고, 그 자체가 실패로 단정할 수 없습니다.
revalidatePath 인자 규칙
- 리터럴 경로(
/vibe-coding/my-post)를 넘길 때는type을 생략합니다. 그 한 페이지만 무효화됩니다. - 패턴(
/vibe-coding/[slug])을 넘길 때는type: 'page'가 필수입니다. 그리고 매칭되는 전부가 무효화됩니다. /page나/layout을 문자열 끝에 붙이지 않습니다. 그것은type으로 표현합니다.
💡 Tip: 성공 기준은 curl 응답이 아니라 라이브 URL에서 새 제목이 보이는지입니다. 상세와 목록 양쪽을 각각 확인하고, 필요하면 2~5회 재시도 smoke로 stale 구간을 넘깁니다.
데이터 변경과 코드 변경 구분
원본의 표는 그대로 유효합니다. 작업 시작 전에 사람에게도 AI에게도 먼저 보여주는 용도입니다.
| 작업 | DB 루프 | 코드 배포 |
|---|---|---|
| 제목, 본문, 태그, 발행일 | ○ | |
| FAQ 같은 JSON 필드 값 추가 | ○ | |
| 오탈자, 출처 보강 | ○ | |
| Markdown 렌더러 문법 추가 | ○ | |
generateMetadata 로직 변경 | ○ | |
| 카드 UI, 레이아웃 | ○ | |
| 새 카테고리(라우팅/메뉴) | 혼합 | 라우팅은 코드 |
| sitemap 생성 로직 | ○ |
전제 하나를 명시해 둡니다. 렌더러가 이미 안정적일 때만 이 루프가 성립합니다. 표가 깨지거나 코드블록이 안 나오면 JSON이 아니라 컴포넌트를 고쳐야 합니다.
⚠️ 주의: AI에게 “이 글 고쳐줘”만 던지면 renderer까지 PR이 나올 수 있습니다. 프롬프트에 “content JSON만 다룬다”를 고정하세요.
revalidate 엔드포인트
App Router 기준 예시입니다. app/api/revalidate/route.ts에 둡니다.
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',
});
}
REVALIDATE_SECRET은 NEXT_PUBLIC_ 접두사를 붙이지 마세요. 붙는 순간 빌드 결과물에 노출됩니다. 또한 성공 판단은 200이 아니라 라이브 smoke 결과로 합니다.
그래서 검증은 이렇게 한다
읽을 값이 두 가지입니다. 본문에 “새 제목” 마커가 있는지, 그리고 캐시 헤더가 stale 구간을 통과했는지입니다. revalidateTag는 기본적으로 stale-while-revalidate 구간을 남기기 때문에, 첫 방문이 옛 화면을 주는 것은 정상 동작일 수 있습니다.
#!/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
검증은 반드시 상세와 목록 양쪽을 봅니다.
./scripts/smoke.sh "https://example.com/vibe-coding/my-post" "새 제목 문자열"
./scripts/smoke.sh "https://example.com/vibe-coding" "새 제목 문자열"조용히 실패하는 세 가지
모든 단계가 성공처럼 보여도 화면만 옛날인 경우는 보통 세 가지입니다. 가장 어려운 유형입니다.
임베디드 리플리카가 옛 데이터를 캐시로 굳힌다
Turso embedded replicas를 쓰면 쓰기는 원격으로 가고, 읽기는 로컬 복제본에서 옵니다. 복제본은 syncInterval 주기로 따라잡습니다. 발행 루프는 upsert 후 곧바로 revalidate를 보내고, 다음 방문 때 렌더러가 로컬 복제본을 읽습니다.
결과적으로 캐시는 “신선한 것처럼” 굳고, 다음 동기화가 끝나도 저절로 갱신되지 않습니다. 다음 발행 때까지 옛 글이 걸려 있는 현상이 나옵니다.
해결은 발행 스크립트가 revalidate 전에 “재생성 요청이 읽는 경로에서 새 값이 보이는지”를 확인하는 것입니다. 쓰기 경로와 읽기 경로가 다르면 항상 생기는 현상입니다.
인스턴스가 둘이면 한 대만 갱신된다
기본 Next.js 캐시는 인스턴스별로 로컬에 붙습니다. revalidateTag는 호출을 받은 인스턴스에서만 무효화됩니다. 로드밸런서 뒤에서 요청이 분산되면 새 글과 옛 글이 번갈아 보일 수 있습니다.
자체 호스팅에서 인스턴스가 여러 개라면 shared cache를 쓰거나, 커스텀 캐시 핸들러로 refreshTags 같은 동기화를 붙여야 합니다.
CDN은 revalidate를 모른다
ISR은 s-maxage와 stale-while-revalidate를 응답합니다. 앞단 CDN이 이 지시자를 존중해 HTML을 캐시하고 있다면, Next.js 내부 캐시 무효화만으로는 HTML이 바로 바뀌지 않습니다.
CDN이 앞단에 있으면 purge가 발행 루프의 필수 단계입니다. revalidate 직후 해당 URL만 골라 purge하고 smoke를 다시 돌리세요.
발행 루프
원본의 6단계에 위 내용을 반영하면 8단계가 됩니다.
| 단계 | 작업 | 통과 기준 |
|---|---|---|
| 1 | 로컬 JSON 작성 | slug, category, title, content 존재 |
| 2 | 스키마 검증 | 필수 필드, 위험 HTML 없음 |
| 3 | 품질 검토(사람) | 얕은 요약 아님, 출처 보강 |
| 4 | DB upsert | id·slug 일치, 영향 행 수 1 |
| 5 | 읽기 경로 확인 | revalidate 이후 렌더러가 새 값을 읽는지 |
| 6 | revalidate + CDN purge(필요 시) | 상세, 목록, 홈, sitemap 모두 200 |
| 7 | live smoke | 상세 URL 마커, 목록 URL 마커 |
| 8 | 재시도(선택) | marker 미도달이면 2~5회 재시도 |
완료 기준은 curl 성공이 아니라 라이브 URL에서 “새 제목이 보이는지”입니다.
되돌리기는 Git revert가 아니다
DB만 바꾸면 Git 히스토리에 흔적이 없습니다. 응급 조치는 상태 토글로 합니다.
UPDATE posts SET status = 'draft', published_at = NULL WHERE slug = 'problem-slug';
이후 revalidate를 호출하면 공개 화면에서 사라집니다. 글 row는 DB에 남아 조사 후 재공개할 수 있습니다. 되돌린 뒤에는 목록과 sitemap도 함께 smoke로 다시 확인하세요.
⚠️ 주의: 되돌릴 때는 상세만 내리지 말고 목록과 sitemap도 함께 무효화하세요. 그렇지 않으면 죽은 링크가 남습니다.
AI에게 맡길 때는 경계를 문장으로 쓴다
나쁜 예는 “AI 뉴스 1개 올려줘”처럼 범위와 검증 경계를 빼먹는 지시입니다. 이 루프에서는 AI를 작성자가 아니라 운영자 절차를 따라 실행하는 도구로 둡니다.
1. content/vibe/<slug>.json에만 파일을 작성한다.
2. validation과 금지어 grep을 통과할 때까지 고친다.
3. publish 스크립트는 승인 후에만 실행한다.
4. 상세 URL과 목록 URL 양쪽에 live smoke를 돌린다.
5. 실패 시 보류 보고하고 다음 수정으로 넘어가지 않는다.
금지: app/, components/, lib/ 이하 파일 수정. DB 직접 접속 금지. 불확실하면 진행 중지와 근거 보고.
체크리스트
작업 전에는 데이터 변경인지 코드 변경인지 구분하고, 로컬 JSON 원본과 되돌리기 방법이 있는지 확인합니다.
검증 중에는 스키마 검증, 금지어 grep, slug/id 일치, 본문 실행 가치가 있어야 합니다.
공개 후에는 revalidate 경로(상세, 목록, 홈, sitemap), live smoke(상세와 목록), 콘솔 오류를 같이 확인합니다.
출처와 링크
- Next.js revalidatePath: 경로별 무효화
- Next.js revalidateTag: 태그 일괄 무효화
- Next.js updateTag: read-your-own-writes
- Next.js Draft Mode: draft 모드 렌더
- Turso embedded replicas: 읽기 지연과 동기화
- Cloudflare Cache Purge: CDN 캐시 잔존 시
조사 기준: 2026년 7월. Next.js 14+ App Router와 Turso CLI 기준입니다. Pages Router나 다른 DB면 API와 스크립트 경로를 바꿔야 합니다.
자주 묻는 질문
revalidate만으로 SEO 메타가 바로 바뀌나요?
generateMetadata가 DB를 읽도록 이미 배포돼 있다면 revalidate 후 다음 요청부터 반영됩니다. 메타 생성 로직 자체를 바꾸려면 코드 배포가 필요합니다. 상세 path와 목록 tag를 같이 revalidate하세요.
DB upsert가 성공했는데 사이트는 옛 글인 이유는?
Next.js와 CDN이 이전 HTML을 캐시하고 있기 때문입니다. upsert 직후 revalidate API를 호출하고, live smoke로 URL에서 새 제목 마커를 확인해야 합니다. Cloudflare를 쓰면 purge도 검토하세요.
Git revert로 발행을 취소할 수 있나요?
DB만 바꾼 발행은 Git 히스토리에 남지 않아 revert로는 안 됩니다. status를 draft로 토글하거나 로컬 JSON 이전 버전을 재업로드한 뒤 revalidate하세요. content/ 폴더를 Git으로 관리하면 복구 기준점이 생깁니다.
FAQ JSON 필드만 추가해도 코드 배포 없이 되나요?
렌더러가 FAQ JSON을 이미 표시하도록 배포돼 있다면 DB 루프만으로 충분합니다. FAQ UI 컴포넌트나 스키마 검증 로직을 바꿔야 하면 코드 배포가 필요합니다. 작업 전 데이터 vs 코드 표로 구분하세요.
revalidate API를 공개 URL에 두어도 되나요?
URL은 공개되어도 x-revalidate-auth 같은 시크릿 헤더 검증은 필수입니다. 검증 없이 열어 두면 누구나 캐시를 깨 DoS에 가까운 부하를 줄 수 있습니다. publish 스크립트와 CI에만 시크릿을 둡니다.
AI에게 글 수정을 맡길 때 꼭 적을 한 줄은?
코드 파일(app/, components/, lib/)은 수정하지 않고 content/ JSON만 다룬다는 문장입니다. validation, publish, smoke 순서와 실패 시 보류 보고도 함께 적으면 renderer PR 사고를 줄일 수 있습니다.
Related terms
본문과 함께 보면 좋은 사전 용어
정적 파일이나 빌드 결과가 아니라 런타임 데이터베이스에 저장된 값을 프론트엔드가 읽어 화면을 구성하는 방식이다. VIBE 코딩에서는 AI가 만든 글, 앱 소개, 사전 항목을 매번 배포하지 않고 DB에 반영한 뒤 공개 페이지에서 읽게 만들 때 중요하다. 이 구조를 쓰면 콘텐츠 변경과 코드 배포를 분리할 수 있지만, 스키마 검증·캐시 갱신·공개 스모크 테스트를 함께 설계해야 한다.
프롬프트 엔지니어링프롬프트 엔지니어링AI 모델에게 원하는 출력을 얻기 위해 입력 텍스트(프롬프트)를 체계적으로 설계하는 기술로, '모델에게 무엇을 물을 것인가'를 다루는 학문이자 실무이다. 같은 AI 모델이라도 프롬프트를 어떻게 구성하느냐에 따라 출력의 품질이 극적으로 달라진다. 예를 들어, '로그인 만들어줘'라는 막연한 프롬프트 대신 'Next.js 14 App Router를 사용하여 이메일/비밀번호 로그인 기능을 구현해줘. bcrypt로 비밀번호 해싱, JWT 토큰 발급, 7일 만료, httpOnly 쿠키 저장. TypeScript strict mode로 작성해줘.'처럼 구체적으로 지시하면 훨씬 정확한 코드를 생성한다. 핵심 기법으로는 제로샷 프롬프팅(예시 없이 직접 지시), 퓨샷 프롬프팅(예시를 먼저 보여준 후 요청), 사고의 연쇄(CoT, 단계별 추론 유도), 시스템 프롬프트(역할·제약 조건 설정) 등이 있다. 2022~2023년에 가장 주목받은 AI 관련 기술이며, 이후 컨텍스트 엔지니어링('모델에게 무엇을 보여줄 것인가')과 하네스 엔지니어링('전체 시스템을 어떻게 운영할 것인가')으로 확장되었지만, 여전히 AI와 소통하는 가장 기본적이고 필수적인 스킬이다.
학습·온보딩스캐폴드프로젝트의 기본 구조(폴더 구조, 설정 파일, 보일러플레이트 코드, 환경 변수 템플릿)를 자동으로 생성하는 것으로, '비계(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, 인증 방식, 디자인 시스템 등)을 반영한 맞춤형 초기 설정을 제공한다는 점에서 차별화된다.
Useful links
실제로 이어서 열어볼 즐겨찾기
쉬운 보안을 지향하는 한국어 보안 계정으로, AI·VIBE 코딩 흐름에서 놓치기 쉬운 보안 감각을 되짚는 데 유용합니다.
VIBE 코딩 레퍼런스웹사이트 해부도 · Website Anatomy MapAI와 웹사이트를 함께 만들 때 ‘그 부분’이 아니라 정확한 UI·웹 용어로 지시할 수 있게 돕는 영-한 시각 사전입니다.
VIBE 코딩 제품 리서치Killed by Google · Google GraveyardGoogle이 종료한 서비스와 제품을 한눈에 모아, 플랫폼 의존성과 제품 지속성 리스크를 판단하게 해 주는 ‘Google 묘지’ 아카이브입니다.
다음 학습
같은 섹션에서 이어 읽기 좋은 콘텐츠
이미지와 파일을 R2에 둘 때 | Hermes용 트래픽과 권한 나누기
R2를 트래픽 무제한 저장소로만 보면, 업로드 경로와 캐시, 버킷 분리를 나중에 붙이다가 깨집니다. Hermes 홈/파일/이미지 서빙을 붙일 때는 공개 URL 경계와 업로드 검증이 본체입니다.
egress가 유리한 것과 업로드/저장/운영 요청 비용이 제로인 것은 다릅니다. 콘솔 요금표를 확인하세요. 공개 put을 열어 두면 스팸 업로드가 먼저 옵니다. 공개 버킷과 비공개 원본 버킷을 나누면 실수로 올린 시크릿이 검색에 노출되는 사고를 줄일 수 있습니다.
커스텀 도메인과 캐시 규칙을 정한 뒤 업로드하세요. URL이 바뀌면 본문 마크다운과 OG 이미지가 한꺼번에 깨집니다. 「이 객체는 누가 읽어도 되는가」에 답이 모호하면 비공개로 둡니다.
바이브코딩으로 애드센스 부업 시작할 때 | 회원/결제 없이 먼저 막는…
바이브코딩으로 「부업 사이트」를 만들 때 첫 프롬프트가 자주 이렇게 나갑니다. 회원가입, 문의폼, 결제, 관리자 대시보드까지 한 번에. 화면은 빨리 나오지만, 그 순간부터 주문/환불/고객 DB/API 키/로그에 쌓인 이메일이 운영자 책임이 됩니다.
애드센스 부업의 본체는 상품을 직접 파는 일이 아닙니다. 유용한 콘텐츠로 방문을 모은 뒤, 광고 지면으로 수익을 받는 간접 수익화입니다. Google이 광고 매칭과 정산을 담당하고, 게시자는 콘텐츠/정책/트래픽 품질을 지킵니다. (How AdSense works)
읽는 데는 약 10분이면 됩니다. 첫 세션에서 실제로 손댈 일은 「무엇을 빼고, 무엇을 고지하고, 어디에 비밀을 두지 않을지」입니다…