무색
기술블로그
에세이
연구
소개

무색

소프트웨어로 비즈니스의 가능성을 만듭니다. 웹·앱 개발, 음성 AI, 자동화 콘텐츠 제작까지 — 기술이 필요한 곳에 무색이 있습니다.

연락처

contact@museck.com

사업자 정보

상호: 무색

대표: 배성재

사업자등록번호: 577-58-00836

인천광역시 연수구 인천타워대로 323, 에이동 8층 801-802호 AB-132 (송도동, 송도 센트로드)

© 2026 무색. All rights reserved.
개인정보처리방침·이용약관·연락처
INCHEON, KR
Next.js 16 캐싱 삽질 오디세이 — isometric 키 비주얼
museck 만들기
2026. 2. 18.

Next.js 16 캐싱 삽질 오디세이: 하루에 12커밋, 세 번의 전략 전환

하루에 12커밋. Cache Components, useCache, ISR — 세 가지 캐싱 전략을 연속으로 시도하고 두 번 실패한 뒤에야 답을 찾았다. Next.js 16의 새로운 캐싱 메커니즘이 아직 얼마나 불안정한지 몸으로 확인한 기록이다.

Next.js 16으로 업그레이드하면서 빌드 환경에 MongoDB가 없는 문제에 부딪혔다. Docker 빌드 시 DB 연결이 안 되니 프리렌더링이 실패한다. 이걸 해결하려고 캐싱 전략을 세 번 바꿨고, 그 과정이 꽤 교훈적이었다.

삼진아웃 흐름

1차 시도: cacheComponents — PayloadCMS admin이 깨졌다

Next.js 16에서 'use cache' 디렉티브가 새로 나왔다. 기존 unstable_cache를 대체하는 깔끔한 API라서 바로 전환했다. cacheComponents: true를 next.config.mjs에 추가하면 된다고 했다.

// next.config.mjs — 넣었다가 바로 뺐다
cacheComponents: true, // PayloadCMS admin과 충돌!

문제는 이 플래그가 dynamicIO를 전역으로 활성화한다는 점이다. dynamicIO가 켜지면 모든 데이터 접근에 Suspense 바운더리가 필요하다. PayloadCMS admin 패널은 그런 구조가 아니라서 빌드가 통째로 깨졌다. 넣은 지 30분 만에 롤백했다.

2차 시도: useCache — 빈 페이지가 영원히 캐시됐다

cacheComponents를 빼고 experimental: { useCache: true }로 바꿨다. 이번에는 빌드가 성공했다. 하지만 배포 후에 문제가 터졌다.

async function getSeriesWithCounts() {
  'use cache'
  cacheTag('series')
  cacheLife('hours')
  // 빌드 타임에 DB 없음 → 빈 배열 반환 → 이게 캐시됨
  const payload = await getPayload({ config })
  // ...
}

Docker 빌드 시점에는 MongoDB가 없다. 'use cache' 함수가 빌드 타임에 실행되면서 빈 배열을 반환했고, 그 빈 결과가 캐시됐다. 문제는 ISR revalidation이 이 캐시를 갱신하지 않는다는 것이다. 빈 페이지가 영원히 서빙됐다.

이건 문서에서 찾을 수 없었던 함정이다. 'use cache'와 "빌드 타임에 DB가 없는 환경"의 조합은 캐시 지옥을 만든다.

3차 시도 (최종): ISR — 결국 검증된 방식으로

두 번 실패하고 나서 ISR로 돌아왔다. 페이지 레벨에서 revalidate = 60을 설정하고, 빌드 타임에는 NEXT_PHASE 가드로 빈 값을 반환하되, 런타임에서 첫 요청 시 실제 데이터로 ISR 캐시를 생성하는 패턴이다.

// 페이지 레벨: ISR
export const revalidate = 60

async function getSeriesWithCounts() {
  // 빌드 타임에는 빈 배열 반환 (DB 없음)
  if (process.env.NEXT_PHASE === 'phase-production-build') return []
  
  // 런타임에서 실제 DB 조회
  const payload = await getPayload({ config })
  return payload.find({ collection: 'posts', /* ... */ })
}

'use cache'는 Header, Footer 같은 레이아웃 컴포넌트에만 유지했다. 이 컴포넌트들은 빌드 타임에 실패해도 런타임에서 try-catch로 복구할 수 있기 때문이다.

사이드 퀘스트: publishedAt 자동 설정

캐싱 삽질 중에 publishedAt 필드를 수동으로 설정하는 게 귀찮아서 훅을 추가했다. 포스트가 처음 publish될 때 자동으로 현재 시간이 들어간다.

// src/hooks/setPublishedAt.ts
export const setPublishedAt: CollectionBeforeChangeHook = async ({
  data, originalDoc
}) => {
  if (data._status === 'published' && !originalDoc?.publishedAt) {
    data.publishedAt = new Date().toISOString()
  }
  return data
}

여기서도 삽질이 있었다. 처음에 previousDoc를 썼는데, PayloadCMS에서 이미 deprecated된 프로퍼티였다. originalDoc으로 바꿔야 했다. 타입이 맞지 않아서 바로 알 수 있었지만, 런타임에서만 확인되는 프레임워크도 있으니 이런 사소한 API 변경은 주의해야 한다.

교훈 정리

  • cacheComponents: true는 dynamicIO를 전역 활성화한다. 이 사실이 문서에 명확하지 않다. PayloadCMS처럼 Suspense 없이 데이터에 접근하는 라이브러리와 충돌한다.
  • 'use cache' + DB 없는 빌드 = 영구 캐시 지옥. 빌드 타임의 빈 결과가 캐시되면 ISR revalidation이 이걸 갱신하지 않는다.
  • Next.js 캐싱은 아직 실험 단계다. 'use cache', cacheComponents, useCache 모두 실험적 기능이다. 프로덕션에서 안정적인 선택은 여전히 ISR이다.
  • NEXT_PHASE 가드는 ISR의 필수 동반자다. DB 없는 빌드 환경에서 빈 값을 반환하되, 런타임에서 실제 데이터로 채우는 패턴이 가장 안정적이었다.

결국 하루에 12커밋을 쌓고 돌아온 곳은 ISR이었다. 새로운 API가 나오면 써보고 싶은 게 개발자 심리인데, "아직 실험적"이라는 경고를 가볍게 보면 안 된다는 걸 다시 배웠다. 검증된 방식이 지루하더라도, 프로덕션에서는 지루한 게 좋다.

자주 묻는 질문

'use cache'는 언제 쓸 수 있나요?

빌드 타임에도 유효한 결과를 반환할 수 있는 함수에 쓰면 안전하다. Header, Footer처럼 실패해도 try-catch로 복구 가능한 레이아웃 컴포넌트가 좋은 대상이다. 페이지 레벨 데이터 함수에는 아직 위험하다.

ISR과 'use cache'를 같은 페이지에서 혼용할 수 있나요?

페이지 레벨은 ISR로 두고, 레이아웃이나 공유 컴포넌트에서 'use cache'를 사용하는 조합은 가능하다. 다만 페이지 데이터 함수 자체에 'use cache'를 걸면 빌드 타임 빈 캐시 문제가 발생할 수 있으니 분리해서 사용해야 한다.

빌드 시 DB가 없는 환경에서 캐싱 전략의 최선은?

ISR + NEXT_PHASE 가드 조합이 현재로서는 가장 안정적이다. 빌드 타임에 빈 값을 반환하고, 런타임 첫 요청 시 실제 데이터로 ISR 캐시를 생성한다. warm-cache 스크립트로 배포 직후 캐시를 미리 생성해두면 첫 사용자도 빈 페이지를 보지 않는다.

자주 묻는 질문

'use cache'는 언제 쓸 수 있나요?
빌드 타임에도 유효한 결과를 반환할 수 있는 함수에 쓰면 안전하다. Header, Footer 같은 레이아웃 컴포넌트가 좋은 대상이다. 페이지 레벨 데이터 함수에는 아직 위험하다.
ISR과 'use cache'를 같은 페이지에서 혼용할 수 있나요?
페이지 레벨은 ISR로 두고, 레이아웃이나 공유 컴포넌트에서 'use cache'를 사용하는 조합은 가능하다. 다만 페이지 데이터 함수 자체에 'use cache'를 걸면 빌드 타임 빈 캐시 문제가 발생할 수 있다.
빌드 시 DB가 없는 환경에서 캐싱 전략의 최선은?
ISR + NEXT_PHASE 가드 조합이 현재로서는 가장 안정적이다. 빌드 타임에 빈 값을 반환하고, 런타임 첫 요청 시 실제 데이터로 ISR 캐시를 생성한다.
museck 만들기(15/22)
Prev

Next.js 셀프호스팅 강화: tini, Build ID, Version Skew 방지까지

Next

콘텐츠 타입 분리: /blog 하나를 세 개로 쪼갠 대수술