블로그 개발일지 #7 — ISR과 Edge Function으로 실시간 배포 구현
결론부터
Supabase에 게시글을 올리면 자동으로 사이트에 반영되는 시스템을 만들었다. ISR (Incremental Static Regeneration)과 Supabase Edge Function을 조합해서, 전체 재배포 없이 변경된 페이지만 재생성한다.
핵심 아키텍처:
flowchart LR
A[Supabase DB 변경] --> B[Database Webhook]
B --> C[Edge Function]
C --> D[Vercel ISR API]
D --> E[페이지 재생성]처음에는 Webhook에서 Vercel API를 직접 호출하려 했지만, Supabase Webhook이 custom payload를 지원하지 않아 막혔다. Edge Function을 중간에 두어 이 문제를 해결했고, 덤으로 보안도 강화되었다.
문제 상황
기존 방식의 불편함
블로그를 Supabase에서 콘텐츠를 가져오는 구조로 만들었다. MCP 서버로 게시글을 작성하면 Supabase에 저장되고, Astro가 빌드 타임에 이 데이터를 가져와 정적 페이지를 생성한다.
문제는 새 글을 쓸 때마다 재배포해야 한다는 것이다:
- Supabase에 글 작성
- Vercel에 재배포 (2-3분 소요)
- 사이트에서 확인
이건 너무 비효율적이다. 특히 오타 수정 같은 작은 변경에도 전체를 재배포해야 한다.
원하는 동작
Supabase에 글을 올리면 즉시 사이트에 반영되어야 한다. 변경된 페이지만 재생성하면 된다.
해결 과정
1단계: ISR 기본 구조
Vercel은 On-Demand Revalidation을 지원한다. 특정 URL에 재생성 요청을 보내면 해당 페이지만 무효화하고 다음 방문 시 재생성한다.
먼저 Astro 설정에 ISR을 활성화했다:
export default defineConfig({
adapter: vercel({
isr: {
expiration: false,
bypassToken: process.env.ISR_BYPASS_TOKEN,
},
}),
});그리고 Revalidation API를 만들었다:
export const POST: APIRoute = async ({ request, site }) => {
const { slug, locale, bypassToken } = await request.json();
if (bypassToken !== import.meta.env.ISR_BYPASS_TOKEN) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
});
}
const pageUrl = `${site}/blog/${slug}`;
await fetch(pageUrl, {
headers: { "x-prerender-revalidate": bypassToken },
});
return new Response(JSON.stringify({ success: true }));
};이제 /api/revalidate에 POST 요청을 보내면 해당 페이지가 재생성된다.
2단계: Database Webhook 시도
Supabase의 Database Webhooks를 사용하면 테이블 변경 시 HTTP 요청을 보낼 수 있다. posts 테이블에 INSERT/UPDATE/DELETE가 발생하면 Vercel API를 호출하면 된다.
문제 발견: Supabase Webhook은 payload를 커스터마이즈할 수 없다.
내가 원한 것:
{
"slug": "{{ record.slug }}",
"locale": "{{ record.locale }}",
"bypassToken": "secret-token"
}실제로 보내지는 것:
{
"type": "INSERT",
"table": "posts",
"record": { "slug": "...", "locale": "...", ... },
"old_record": null
}bypassToken을 payload에 넣을 방법이 없다. 문서를 찾아봐도 템플릿 문법은 없었다.
3단계: Edge Function 도입
해결책은 Supabase Edge Function을 중간에 두는 것이다:
flowchart TB
subgraph Before["❌ Before"]
W1[Webhook] -.X.-> V1[Vercel API]
end
subgraph After["✅ After"]
W2[Webhook] --> E[Edge Function]
E --> V2[Vercel API]
endEdge Function이 하는 일:
- Webhook의 자동 payload 받기
slug,locale추출- 환경변수에서
bypassToken가져오기 - Vercel Revalidation API 호출
Edge Function 코드:
import { serve } from "https://deno.land/std@0.168.0/http/server.ts";
serve(async (req) => {
const payload = await req.json();
const bypassToken = Deno.env.get("ISR_BYPASS_TOKEN");
const siteUrl = Deno.env.get("SITE_URL");
const record = payload.record || payload.old_record;
const { slug, locale = "ko" } = record;
if (payload.type !== "DELETE" && record.draft === true) {
return new Response(JSON.stringify({
success: true,
skipped: true
}));
}
const response = await fetch(`${siteUrl}/api/revalidate`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug, locale, bypassToken }),
});
return new Response(JSON.stringify(await response.json()));
});이제 Webhook은 Edge Function을 호출하고, Edge Function이 Vercel을 호출한다.
구현 세부사항
환경변수 접근 문제
Astro API 라우트에서 process.env로 환경변수에 접근하면 undefined가 나온다. Vite가 빌드 타임에 import.meta.env로 주입하기 때문이다.
// ❌ 작동 안 함
const token = process.env.ISR_BYPASS_TOKEN;
// ✅ 작동함
const token = import.meta.env.ISR_BYPASS_TOKEN;URL 생성 시 이중 슬래시
site 객체가 trailing slash를 포함하는데, 경로도 /로 시작해서 //가 생겼다:
// ❌ https://site.com//blog/post
const url = `${site}/${locale}/blog/${slug}`;
// ✅ https://site.com/blog/post
const siteUrl = site.toString().replace(/\/$/, "");
const url = `${siteUrl}/${locale}/blog/${slug}`;TypeScript 타입 체크 에러
Edge Function은 Deno 환경이라 tsconfig.json에 포함되면 에러가 난다:
{
"exclude": ["dist", "node_modules", "supabase"]
}supabase 디렉토리를 통째로 제외했다.
배포 및 설정
Vercel 설정
환경변수 추가:
ISR_BYPASS_TOKEN:openssl rand -base64 32로 생성한 토큰SITE_URL: 프로덕션 도메인
Supabase Edge Function 배포
supabase login
supabase link --project-ref <project-id>
supabase secrets set ISR_BYPASS_TOKEN=<token>
supabase secrets set SITE_URL=https://your-site.com
supabase functions deploy revalidate-blogDatabase Webhook 설정
Supabase Dashboard에서:
- Database → Webhooks → Create a new hook
- Table:
posts - Events: INSERT, UPDATE, DELETE
- Type: Supabase Edge Functions
- Edge Function:
revalidate-blog
작동 검증
전체 흐름
sequenceDiagram
participant U as User (MCP)
participant S as Supabase
participant W as Webhook
participant E as Edge Function
participant V as Vercel
participant C as CDN
U->>S: 게시글 작성/수정
S->>W: INSERT/UPDATE 이벤트
W->>E: Webhook payload
E->>E: slug, locale 추출<br/>bypassToken 주입
E->>V: POST /api/revalidate
V->>V: 페이지 무효화
V->>C: 캐시 삭제
C-->>U: 다음 방문 시<br/>재생성된 페이지 제공로컬 테스트
API를 직접 호출해서 확인:
curl -X POST http://localhost:4321/api/revalidate \
-H "Content-Type: application/json" \
-d '{
"slug": "test-post",
"locale": "ko",
"bypassToken": "local-token"
}'응답:
{
"success": true,
"revalidated": [
"http://localhost:4321/blog/test-post",
"http://localhost:4321/blog"
]
}프로덕션 테스트
- MCP로 게시글 작성
- Supabase Webhook History 확인 →
200 OK - Edge Function 로그 확인:
supabase functions logs revalidate-blog --limit 5 - 사이트에서 확인 → 새 글이 즉시 보임 ✅
얻은 것
보안 강화
Before: Webhook payload에 토큰 노출 (Supabase Dashboard에서 볼 수 있음) After: Edge Function 환경변수에 저장 (외부 노출 없음)
유연성 확보
Edge Function에서 비즈니스 로직 추가 가능:
draft: true게시글 필터링- 특정 조건에서만 재생성
- 여러 페이지 동시 무효화
- 로깅 및 모니터링
비용 효율
Vercel ISR:
- 변경된 페이지만 재생성 (전체 빌드 불필요)
- CDN에서 캐싱 (Serverless Function 호출 최소화)
Supabase Edge Function:
- 무료 티어: 월 500K 호출
- 게시글 변경이 드물면 무료 범위 내
아쉬운 점
Webhook payload 커스터마이즈 불가
Supabase가 템플릿 문법을 지원했다면 Edge Function 없이도 가능했을 것이다. 하지만 Edge Function 덕분에 더 나은 아키텍처가 되었으니 결과적으로는 좋다.
완전한 실시간은 아님
무효화 후 첫 방문 시 재생성이라 약간의 지연이 있다. Stale-While-Revalidate 전략으로 재생성 중에도 이전 버전을 제공하지만, 엄밀히는 실시간이 아니다.
완전한 실시간을 원하면 SSR로 전환해야 하는데, 블로그에는 과하다.
다음 단계
선택적 무효화
현재는 게시글 페이지와 목록 페이지를 모두 무효화한다. 필요시에만 목록을 무효화하도록 개선할 수 있다.
모니터링 강화
Edge Function과 Vercel 로그를 통합해서 대시보드를 만들면 좋겠다. 어떤 페이지가 언제 재생성되었는지 추적하는 시스템.
이미지 최적화
블로그 이미지도 Supabase Storage에 올리고, 변경 시 자동으로 최적화하는 파이프라인을 만들 계획이다.
정리
문제: Supabase에 글을 올려도 재배포 전까지 사이트에 안 보임 원인: 정적 사이트라 빌드 타임에만 데이터를 가져옴 해결: ISR + Edge Function으로 변경된 페이지만 재생성
핵심 인사이트: 제약이 있을 때 중간 계층을 두면 해결되는 경우가 많다. Webhook의 제약을 Edge Function으로 우회했더니 보안과 유연성까지 얻었다.
이제 블로그에 글을 쓰는 경험이 훨씬 부드러워졌다. Supabase에 저장하면 끝. 재배포를 기다릴 필요가 없다.