본문으로 건너뛰기
코드룸CODE ROOMTENMO ↗

Next.js ISR 비용 절감 가이드: force-dynamic 탈출로 Vercel 비용과 DB 부하 잡기

ISR 캐싱 주기 설정과 revalidatePath 무효화, Redis 연동으로 Vercel 비용과 DB 부하를 줄이는 구성을 정리했습니다.

매 요청마다 렌더링하는 force-dynamic이 지갑을 갉아먹는 이유

Next.js 프로젝트를 배포한 뒤, 트래픽이 늘어남에 따라 Vercel 청구 금액이나 데이터베이스(DB) CPU 사용량이 선형으로 치솟는 경험을 해보셨을 겁니다. 이 문제의 대부분은 모든 페이지를 매 요청마다 새로 그리는 force-dynamic(또는 무분별한 SSR) 설정에서 비롯됩니다. 사용자가 100명, 1,000명 동시 접속할 때마다 서버는 똑같은 DB 조회를 반복하고, 똑같은 HTML을 만드느라 연산 자원을 낭비합니다. 이 악순환을 끊어내고 서버 비용을 고정 비용에 가깝게 줄여주는 마법이 바로 ISR입니다.

1. ISR의 비용 절감 메커니즘

핵심은 "서버가 매번 DB를 조회해 HTML을 새로 만드는가" 대 "이미 만든 HTML을 일정 시간 캐시해 재사용하는가"의 차이입니다.

[force-dynamic: 매 요청 렌더링]
유저 A → DB 조회 → HTML 생성 → 응답 (~500ms)
유저 B → DB 조회 → HTML 생성 → 응답 (~500ms)
※ 100명 동시 접속 시 = DB 조회 100번 발생 (비용 선형 증가)

[ISR: revalidate = 60 설정]
유저 A → DB 조회 → HTML 생성 + 캐시 저장 → 응답 (~500ms)
유저 B (30초 후) → 캐시된 HTML 즉시 반환 (~50ms)
유저 C (70초 후) → 캐시 반환 + 백그라운드에서 HTML 조용히 재생성
※ 100명 동시 접속 시 = 60초당 DB 조회 단 1번 (비용 고정)

비교 항목

force-dynamic (SSR)

ISR (revalidate=60)

응답 속도 (TTFB)

매번 500ms 이상 (서버 연산 필요)

캐시 히트 시 ~50ms 미만

DB / API 부하

요청당 무조건 1회 발생

캐시 주기당 딱 1회 발생

인프라 비용

트래픽에 비례하여 선형 증가

트래픽이 늘어도 비용 거의 고정

SEO 크롤링 최적화

응답 속도가 상대적으로 느림

극도로 빠른 응답으로 크롤러 가산점

2. 콘텐츠 성격별 revalidate 골든 타임 선택 원칙

모든 페이지에 똑같은 캐시 주기를 적용할 수는 없습니다. 콘텐츠의 변경 빈도에 따라 영리하게 라우트별 revalidate 설정을 분리해야 합니다.

  • 실시간성이 아주 높지 않은 목록 페이지 (60초 ~ 300초)

    • 홈 화면, 제품 리스트, 최신 저널 목록 등은 짧은 캐시(1분~5분)로도 수천 명의 동시 접속 요청을 방어할 수 있습니다.

  • 자주 바뀌지 않는 상세 페이지 (86400초 = 하루)

    • 제품 상세, 성분 정보, 브랜드 소개 등은 캐시 주기를 하루 이상 길게 잡습니다. 대신 콘텐츠가 수정되었을 때 revalidatePath()를 호출하여 수동으로 캐시를 뚫어주는(Purge) 전략을 취합니다.

  • 캐시를 절대 적용하면 안 되는 페이지 (force-dynamic 유지)

    • 사용자 개인화 페이지(마이페이지, 장바구니), 어드민 관리자 페이지, 동적 API 피드 등은 캐싱 대상에서 제외하는 것이 올바른 선택입니다.

3. HTML 캐시(ISR)와 데이터 캐시(Redis)의 2중 레이어 아키텍처

렌더링 결과물인 HTML을 캐싱하는 ISR만으로도 강력하지만, DB 원천 데이터를 보호하기 위해서는 백엔드 단의 Redis 캐시 레이어를 함께 조합하는 것이 정석입니다.

[유저 요청] ──> [1차: Next.js ISR (HTML 캐시)] 
                       │ (Cache Miss 시)
                       ▼
                [2차: Redis 캐시 (getOrSetJson)] ──> [최종: DB 조회]
  1. ISR (프론트엔드 캐시): revalidate 옵션을 통해 렌더링된 컴포넌트 결과물 자체를 재사용합니다.

  2. Redis (데이터 캐시): 복잡한 애그리게이션 쿼리나 브랜드·성분 목록처럼 무거운 DB 조회 결과를 getOrSetJson 패턴으로 메모리에 올립니다.

  3. 수동 무효화(On-Demand Revalidation): 어드민이 백엔드에서 데이터를 수정하는 순간 Redis의 키를 삭제(deleteKeys)하고, Next.js의 revalidatePath('/products/123')를 호출하여 유저에게 즉시 최신 데이터가 반영되도록 동기화합니다.

4. 추가로 쥐어짜는 인프라 비용 절감 팁

① 목록 쿼리 Payload 최소화 (.select() 최적화)

목록 페이지를 불러올 때 무거운 본문 텍스트(content, description)까지 전부 DB에서 가져와 직렬화하고 계시진 않나요? 목록 전용 데이터 추출 시 .select()를 활용해 본문을 과감히 제외하고, 미리 요약된 excerpt 필드만 로드하세요. DB I/O와 메모리 소비량이 극적으로 줄어듭니다.

② 이미지 최적화 비용 방어

Vercel 아키텍처에서 은근히 많은 비용을 차지하는 주범은 /_next/image 파이프라인입니다. 이미지 컴포넌트의 sizes 속성을 실제 렌더링되는 크기에 맞게 좁고 촘촘하게 정의해 두어야 생성되는 리사이징 변형(Width) 수가 줄어들어 불필요한 이미지 최적화 과금을 막을 수 있습니다.

자주 묻는 질문

  • Q1. revalidate 주기를 하루(86400초)로 잡았을 때, 관리자가 글을 수정하면 유저는 하루 동안 옛날 글만 보게 되나요?

    • A1. 아닙니다. 관리자가 글을 수정하는 시점에 Next.js에서 제공하는 revalidatePath() 또는 revalidateTag() API를 호출해주면, 설정된 주기와 상관없이 즉시 백그라운드 캐시가 무효화되어 다음 접속자부터 곧바로 새 페이지를 보게 됩니다.

  • Q2. dynamicParams = false 설정은 어떤 경우에 쓰나요?

    • A2. 미리 빌드 타임에 생성해 둔 정적 라우트(예: 다국어 로케일 등) 외에 다른 동적 파라미터 요청이 들어왔을 때, 404 에러를 즉시 반환하도록 제한하는 설정입니다. 의도하지 않은 무작위 URL 요청으로 인해 서버 캐시 리소스가 낭비되는 것을 방지할 때 유용합니다.

  • Q3. 목록에서 본문을 제외하는 excerpt 최적화가 Next.js 렌더링 비용에도 영향을 주나요?

    • A3. 매우 큰 영향을 줍니다. DB에서 가져오는 페이로드 크기가 줄어들면 Node.js 서버 데이터 직렬화(Serialization) 과정에서 CPU 소모가 줄어들고, 결과적으로 서버리스 함수의 실행 시간이 단축되어 전체적인 인프라 과금 감소로 이어집니다.

블로그 목록으로