Next.js 15 App Router 캐싱 전략과 부분 사전 렌더링 완벽 정리
Next.js 15의 변경된 캐싱 기본값, 부분 사전 렌더링(PPR), 서버 액션을 활용한 렌더링 최적화 전략을 실무 관점에서 정리한다. revalidate 태그와 스트리밍 팁까지 담았다.
Next.js 15 App Router 캐싱 전략과 부분 사전 렌더링 완벽 정리
Next.js는 App Router 도입 이후 가장 큰 변화를 15 버전에서 맞았다. 2024년 10월 정식 출시된 15는 캐싱 기본값을 대대적으로 바꾸며 개발자들이 겪던 예측 불가능한 캐시 동작 문제를 정면으로 다뤘다. 여기에 부분 사전 렌더링이 실험 단계를 지나 실전 도입이 늘면서, 정적 렌더링과 동적 렌더링의 경계가 새롭게 정의되고 있다. 이 글은 App Router 기반 프로젝트의 캐싱과 렌더링 전략을 최신 기준으로 정리한다.
캐싱 기본값의 전환
14 버전까지 fetch 요청과 라우트 핸들러는 기본적으로 캐시되었고, 이 때문에 데이터가 갱신되지 않는 문제로 많은 개발자가 혼란을 겪었다. 15에서는 이 기본값이 뒤집혀, fetch와 GET 라우트 핸들러, 클라이언트 라우터 캐시가 기본적으로 캐시되지 않도록 바뀌었다. 이제 캐싱은 명시적 선택이 되었고, 이는 직관적이지만 성능을 위해서는 캐싱을 의도적으로 설계해야 함을 뜻한다.
정적 데이터는 fetch 옵션에서 캐시를 강제로 켜거나, 라우트 세그먼트 수준에서 재검증 주기를 지정한다. 데이터가 변경되는 시점을 정확히 아는 경우, 시간 기반 재검증보다 온디맨드 재검증이 훨씬 효율적이다. 콘텐츠를 태그로 묶어두고 변경 이벤트가 발생할 때 해당 태그만 무효화하면, 불필요한 재생성 없이 최신 상태를 유지한다.
// 태그 기반 캐싱과 온디맨드 재검증
const posts = await fetch('https://api.example.com/posts', {
next: { tags: ['posts'], revalidate: 3600 },
});
// 변경 발생 시 서버 액션에서 revalidateTag('posts') 호출
부분 사전 렌더링의 원리
부분 사전 렌더링은 하나의 페이지 안에서 정적 부분과 동적 부분을 자동으로 분리하는 기술이다. 페이지의 정적 셸을 빌드 타임에 미리 생성해 즉시 응답하고, 사용자별로 달라지는 동적 영역은 Suspense 경계로 감싸 스트리밍으로 채운다. 결과적으로 사용자는 정적 셸을 거의 즉시 받아보고, 개인화된 콘텐츠는 준비되는 대로 이어서 표시된다.
이 접근의 장점은 개발자가 정적과 동적을 수동으로 나누지 않아도 된다는 점이다. Suspense 경계만 적절히 배치하면 프레임워크가 알아서 렌더링 전략을 최적화한다. 예를 들어 상품 상세 페이지에서 상품 정보와 이미지는 정적 셸에 포함되고, 로그인 사용자의 장바구니 상태나 추천 목록은 동적으로 스트리밍된다. 이로써 캐시 가능한 부분의 이점과 실시간성을 동시에 확보한다. 다만 동적 데이터에 접근하는 함수를 Suspense 밖에서 호출하면 페이지 전체가 동적으로 전환되므로, 경계 설계가 성능을 좌우한다.
서버 액션과 스트리밍 실무
서버 액션은 폼 제출과 데이터 변경을 서버에서 직접 처리하는 방식으로, 별도 API 라우트를 만들지 않고도 뮤테이션을 구현한다. 15에서는 서버 액션의 보안이 강화되어, 사용되지 않는 액션은 자동으로 데드 코드 제거되고 액션 ID가 예측 불가능하게 생성된다. 다만 서버 액션은 여전히 공개 엔드포인트이므로, 내부에서 반드시 인증과 권한 검사를 수행해야 한다.
스트리밍을 잘 활용하려면 로딩 UI를 세분화해야 한다. loading 파일로 라우트 전체 로딩을 처리하는 동시에, 페이지 내부에서 Suspense를 중첩해 개별 컴포넌트 단위로 폴백을 제공하면 체감 성능이 크게 향상된다. 또한 15부터 안정화된 터보팩 기반 개발 서버는 대규모 프로젝트의 재컴파일 속도를 크게 줄여, 반복 개발 사이클을 단축한다. 마이그레이션 시에는 공식 codemod로 비동기 요청 API 변경을 일괄 적용한 뒤, 캐싱 동작을 페이지별로 검증하며 넘어가는 것이 안전하다. 프로덕션 배포 전 렌더링 모드를 빌드 출력에서 확인해, 의도한 정적과 동적 구분이 실제로 적용됐는지 반드시 점검해야 한다.
데이터 캐시 계층의 이해와 성능 지표
Next.js의 캐싱은 여러 계층으로 이뤄져 있어 전체 그림을 이해해야 예측 가능한 동작을 얻는다. 서버 쪽에는 개별 데이터 요청을 저장하는 데이터 캐시와, 렌더링된 라우트 세그먼트를 저장하는 풀 라우트 캐시가 있다. 클라이언트 쪽에는 방문한 라우트를 메모리에 담아 즉시 재방문을 처리하는 라우터 캐시가 있다. 어떤 데이터가 어느 계층에 얼마나 오래 머무는지를 명확히 설계하지 않으면, 오래된 데이터가 표시되거나 반대로 캐싱 이점을 전혀 누리지 못하는 문제가 생긴다.
동적 렌더링을 의도적으로 강제해야 하는 경우도 있다. 요청마다 반드시 최신 데이터가 필요한 대시보드나 개인화 페이지에서는 세그먼트 설정으로 동적 렌더링을 명시하거나, 요청 헤더와 쿠키에 접근해 자연스럽게 동적으로 전환한다. 반대로 정적 생성이 가능한 콘텐츠는 빌드 타임에 미리 생성하고, 새 콘텐츠가 추가되면 온디맨드로 개별 경로만 재생성하는 방식이 서버 부하와 응답 속도의 균형을 맞춘다.
성능을 실질적으로 개선하려면 이미지와 폰트 최적화도 함께 챙겨야 한다. 내장 이미지 컴포넌트는 자동으로 적절한 크기와 포맷을 제공하고 지연 로딩을 처리해, 레이아웃 이동을 방지하면서 전송량을 줄인다. 폰트는 빌드 타임에 자체 호스팅되어 외부 요청과 렌더링 차단을 제거한다. 이러한 최적화의 효과는 반드시 실제 사용자 환경의 핵심 웹 지표로 검증해야 하며, 배포 후에도 지속적으로 모니터링해 회귀를 조기에 잡아내는 체계를 갖추는 것이 중요하다.
결국 최신 렌더링 프레임워크를 잘 다루는 핵심은 도구의 동작 원리를 이해하고 의도적으로 선택하는 태도에 있다. 어떤 데이터가 언제 갱신되어야 하는지, 어느 부분이 정적이고 어느 부분이 사용자마다 달라지는지를 먼저 명확히 정의한 뒤, 그에 맞는 렌더링 전략과 캐싱 정책을 적용해야 한다. 프레임워크의 기본값에만 의존하면 예상과 다른 동작에 당황하기 쉽지만, 각 계층의 역할을 파악하고 나면 성능과 최신성을 모두 만족하는 정교한 설계가 가능해진다. 이러한 이해를 바탕으로 팀이 일관된 규칙을 세우고 이를 코드 리뷰와 문서로 공유할 때, 프로젝트가 커져도 성능과 유지보수성을 함께 지킬 수 있다.