Development··2분 읽기·0

Next.js + Supabase 블로그 성능 최적화: 정적 생성의 함정과 ISR

Vercel 배포 후 DB 수정이 반영되지 않고, 페이지 로딩도 느려진 문제를 ISR로 해결한 과정

글꼴

상황

Next.js 16 + Supabase로 블로그를 만들고 Vercel에 배포했다. 로컬에서는 잘 동작하는데, 프로덕션에서 두 가지 문제가 발생했다.

문제 1: DB 수정사항이 반영되지 않음

  • Supabase에서 포스트를 수정해도 사이트에 변화 없음
  • 새 포스트를 추가해도 목록에 안 보임

문제 2: 페이지 로딩이 느림

  • 첫 페이지 로딩에 체감상 1초 이상 소요
  • 새로고침할 때마다 느림

첫 번째 시도: force-dynamic

처음에는 "DB 연결 문제인가?"라고 생각했다. 하지만 API는 정상 작동했다.

실제 원인은 Next.js의 정적 생성(Static Generation) 이었다.

// 이렇게만 작성하면 빌드 시점에 정적으로 생성됨
export default async function BlogPage() {
  const posts = await getAllPostsAsync()
  return <PostList posts={posts} />
}

Next.js App Router는 기본적으로 페이지를 정적으로 생성한다. 빌드할 때 Supabase에서 데이터를 가져와서 HTML로 만들어두고, 이후 요청에는 그 HTML을 그대로 제공한다.

그래서 빌드 이후에 DB를 수정해도 반영되지 않았던 것이다.

해결: force-dynamic 적용

// 매 요청마다 서버에서 렌더링
export const dynamic = 'force-dynamic'
 
export default async function BlogPage() {
  const posts = await getAllPostsAsync()
  return <PostList posts={posts} />
}

이렇게 하면 매 요청마다 서버에서 Supabase를 조회하고 렌더링한다. DB 수정사항이 즉시 반영되었다.

문제 1 해결! ...인 줄 알았다.

두 번째 문제 발생: 느려진 페이지 로딩

force-dynamic 적용 후 페이지가 눈에 띄게 느려졌다. curl로 측정해봤다.

curl -w "TTFB: %{time_starttransfer}s\nTotal: %{time_total}s\n" \
  -o /dev/null -s "https://my-blog.vercel.app/"

결과:

TTFB: 0.831s
Total: 1.038s

TTFB(Time To First Byte)가 0.8초. 정적 페이지라면 0.1초 미만이어야 정상이다.

느린 이유

force-dynamic은 매 요청마다:

  1. Vercel Serverless 함수 실행 (cold start 포함)
  2. Supabase DB 쿼리 실행
  3. React 서버 사이드 렌더링
  4. 응답 전송

이 모든 과정을 거친다. 캐싱이 전혀 없으니 당연히 느릴 수밖에 없다.

최종 해결: ISR (Incremental Static Regeneration)

실시간 반영이 정말 필요할까? 블로그 특성상 수정 후 몇 분 정도 딜레이는 괜찮다.

ISR을 사용하면:

  • 캐시된 페이지를 빠르게 제공 (정적처럼)
  • 지정 시간마다 백그라운드에서 자동 재생성
  • DB 변경사항도 반영됨 (약간의 딜레이)
// ISR: 10분마다 백그라운드 재생성
export const revalidate = 600
 
export default async function BlogPage() {
  const posts = await getAllPostsAsync()
  return <PostList posts={posts} />
}

ISR 작동 방식

  1. 첫 요청: 페이지 생성 후 캐시
  2. 600초(10분) 동안: 캐시된 페이지 즉시 제공 (빠름)
  3. 600초 후 첫 요청: 백그라운드에서 재생성 시작
  4. 재생성 완료 후: 새 페이지로 캐시 교체
  5. 다음 요청부터: 새 페이지 제공

사용자는 항상 빠른 응답을 받고, 데이터도 최대 10분 딜레이로 반영된다.

적용 결과

모든 데이터 페칭 페이지에 revalidate = 600 적용:

// src/app/page.tsx (홈)
// src/app/blog/page.tsx (블로그 목록)
// src/app/blog/[slug]/page.tsx (포스트 상세)
// src/app/category/page.tsx, [slug]/page.tsx
// src/app/series/page.tsx, [slug]/page.tsx
// src/app/tags/page.tsx, [tag]/page.tsx
// src/app/notes/page.tsx

예상 결과:

  • TTFB: 0.8초 → 0.1초 이하
  • 데이터 반영: 즉시 → 최대 10분 딜레이

정리: 언제 무엇을 쓸까

방식언제 사용장점단점
정적 생성 (기본)데이터가 변하지 않는 페이지가장 빠름빌드 후 데이터 변경 불가
force-dynamic실시간 데이터가 필수인 경우항상 최신 데이터느림, 서버 부하
ISR (revalidate)대부분의 경우빠름 + 데이터 갱신약간의 딜레이

블로그, 마케팅 사이트, 문서 사이트 등 대부분의 경우 ISR이 정답이다. 실시간성이 중요한 대시보드나 채팅 같은 경우에만 force-dynamic을 사용하자.

배운 점

  1. Next.js 기본 동작을 이해하자: App Router는 기본적으로 정적 생성한다
  2. 성능 측정부터: "느린 것 같다"가 아니라 TTFB를 측정하자
  3. 트레이드오프를 이해하자: 실시간성 vs 성능, 적절한 균형점 찾기
  4. ISR은 좋은 기본값: 대부분의 상황에서 ISR이 최적의 선택

이 글이 어떠셨나요?

이 글이 도움이 되셨나요?
공유:

관련 포스트

뉴스레터 구독

새 글이 올라오면 이메일로 알려드려요.

댓글

댓글을 불러오는 중...