PPR과 Cacheable Component 기능이 Nextjs 정식 기능으로 편입하면서 캐싱 전략에 큰 변화가 생겼어요.
기존의 SSR Streaming방식은 화면 전체를 항상 다시 그려야했지만, PPR이 정식 기능으로 변경되면서 정적인 부분은 PreRendered 되게 만들고 나머지 동적인 부분만 새롭게 그려 채워넣을 수 있게 되었어요.
즉 Static과 Dynamic 페이지 중 1가지 선택을 강요하는 기존 SSR Streaming 방식에서 static, cached, dynamic 콘텐츠를 하나의 route에서 섞어쓸 수 있는 PPR 방식을 사용할 수 있게 된거에요.
이번 글에서는 ISR 캐싱 방식(게시글, 카테고리 SSG)에서 PPR 캐싱 방식으로 전환하면서 겪은 어려움을 전달하고 저가 생각하는 트레이드 오프를 전달하려고 해요.
Nextjs 15 | Nextjs 16 |
|---|---|
next.js App Router의 Page의 ISR 캐싱 코드 domain 패키지의 게시글 api | next.js에서 PPR 사용을 위한 설정 next.js에서 게시글 컴포넌트 PPR 코드 |
위 코드를 보면 캐싱 적용하는 방식이 완전히 달라졌음을 알 수 있어요.
ISR을 사용할 때는 revalidate라는 route segment config를 통해서 페이지를 캐싱하고, next.js에서 wrapping한 fetch 함수에 있는 revalidate 옵션을 통해 api 결과를 원하는 시간동안 캐싱해요.
PPR을 사용할 때는 파일 단위/컴포넌트(함수) 단위로 "use cache" 지시어로 캐싱할 수 있고 cacheLife, cacheTag로 캐싱 시간과 캐싱 태그를 지정할 수 있어요. 그리고 기존의 route segment config는 사용하지 못하게되요. 물론 fetch 함수의 revalidate 옵션은 여전히 사용 가능해요.
그리고 두 방식을 직관성, 사용성, 의존성 분리 측면에서 비교해볼게요.
직관성 측면에서 ISR 방식에서는 revalidate가 뭘 위한 값인지 바로 알기 힘들어요. 하지만 PPR 방식에서는 "use cache"라는 지시어로 정확히 뭘 하고싶은 것인지 의도가 드러나요.
사용성 측면에서는 ISR 방식은 All Static하게 가거나 부분 Streaming을 한다해도 All Dynamic하게 페이지를 렌더링할 수 밖에 없어요. 하지만 PPR 방식에서는 Layout 부분은 정적으로 PreRendered되게 사용하며, Content만 동적으로 렌더링할 수 있고 심지어는 캐싱해서 스트리밍 해줄 수 있어요.
의존성 분리 측면에서는 기존의 캐싱 방법에서는 api 결과를 캐싱하기 위해 api layer를 항상 침범했어요. next.js에서 fetch 함수에 revalidate 속성을 추가해서 캐싱하도록 만든 것이 caching과 api layer의 강한 결합을 만들어 버렸어요. 하지만 "use cache" 방식에서는 함수 결과를 캐싱할 수 있기 때문에 api layer를 침범하지 않고 캐싱하는게 가능해졌어요. (api layer 결합을 분리할 수 있어서 정말 속이 뻥 뚫리는 것 같아요!!!!! 이 거 때문에 monorepo에서 패키지로 분리한 api layer에서 type declare 할 일이 없어졌어요.)
구분 | ISR (Incremental Static Regeneration) | PPR (Partial Prerendering) |
|---|---|---|
기본 개념 | 페이지 전체를 정적으로 생성 + 주기적 재생성 | 페이지를 정적 + 동적 조각으로 분리 |
프리렌더 범위 | Page 단위 | Route 내 Subtree 단위 |
동적 콘텐츠 처리 | 전체 페이지 재생성 필요 | 동적 부분만 런타임 렌더링 |
캐싱 기준 | revalidate (시간 기반) | "use cache", cacheLife, cacheTag 기반 |
HTML 생성 시점 | 빌드 시 + 재검증 시 | 빌드 시(정적) + 요청 시(동적) |
사용자 응답 | 항상 완성된 HTML | 정적 HTML 즉시 + 동적 스트리밍 |
TTFB | 매우 빠름 | 빠름 (정적 쉘 즉시 전달) |
TTI | 페이지 크기에 비례 | 동적 부분만 지연 |
재검증 비용 | 페이지 전체 | 필요한 subtree만 |
ISR에서 PPR로 전환하면서 성능적으로 어떻게 변했는지 측정했어요. (근데 글꼴 다운로드 방식을 실수로 함께 반영해서 측정에 영향이 있어서 아쉬워요.)
디바이스 성능 측정 방법
내 디바이스 성능
타겟 디바이스 성능
Lighthouse Benchmark 점수 4246
Low-End Desktop을 타겟팅
lighthouse cpu benchmark 점수가 4246이기 때문에, CPU Throttling 4X를 적용하여 1000-1500점 사이가 나오도록 조정하여 측정하였습니다.
네트워크 속도는 데스크탑 환경이지만 안좋은 네트워크 환경을 고려해 [4GFast]를 타겟팅하여 측정했습니다.[4GFast]
label="4G Fast (10 Mbps, 40ms RTT)"
bwIn=10240000
bwOut=10240000
latency=40
최종 성능 측정 명령어는 [CPU Throttling 4X, latency 40, 10Mbps]를 적용했습니다.lighthouse --throttling-method=devtools
--preset=desktop
--throttling-method=devtools
--throttling.requestLatencyMs=40
--throttling.downloadThroughputKbps=10000
--throttling.uploadThroughputKbps=10000
--throttling.cpuSlowdownMultiplier=4
https://yooncarrot.com/category/Introduction/Blog또한 성능 도구에서도 [CPU Throttling 4.0X, 빠른 4G]를 이용해 측정했습니다.
CPU : | ISR 캐싱 전략 | PPR 캐싱 전략 |
|---|---|---|
RSC Payload 크기 | ![]() | ![]() |
FCP (First Contentful Paint) INP (Interaction to Next Paint) CLS (Cumulative Layout Shift) | ![]() ![]() ![]() | ![]() ![]() |
| ||
전체적으로 중요한 키 포인트는 RSC Payload 크기, INP 지표, FCP 지표 개선이에요. Static한 일부만 Prerendering하기 때문에 INP와 FCP가 대폭 개선되고, RSC Payload도 Streaming 하는 부분만 가져오기 때문에 크기가 대폭 감소해서 서버 자원 소모, 네트워크 비용, 사용자 경험이 개선되었을 것으로 판단되요.
전통적(Next.js 16 이전)으로 Next.js Server Component, Client Component, dynamic, SSG, ISR, SSR, CSR, PPR 등 다양한 개념들이 Next.js에 존재해요. 한번 분류해봐요.
HTML 렌더링 시점 (캐싱 여부 + 시점)
SSG -> 빌드 시점
ISR -> 빌드 시점 + revalidate 시점
SSR -> 요청 시점
CSR -> 클라이언트 필요시
코드 실행 환경
Server Component (서버)
Client Component (서버 + 클라이언트)
dynamic + ssr:false (클라이언트)
HTML 렌더링 시점 전략 조합
PPR (SSG + SSR + ISR)
이제 위의 분류가 정확히 머리에 정립이 되셨다면 각각 어느 경우에 사용해야하는 개념인지도 살펴보는게 좋을 것 같아요. 저 나름대로 나눠보긴 했는데 실제로는 너무 다양한 상황이 나와서 항상 정답은 아니라는 것 명심하셔야해요.
캐싱
SSG가 적합한 경우
완전 정적인 페이지 (빌드시 만들어도 되는 랜딩 페이지, 문서 페이지 등)
ISR이 적합한 경우
동적이나 잘 안바뀌는 페이지 (블로그 글, 뉴스 글, 상품 상세)
SSR이 적합한 경우 (캐싱 안함)
요청 시점 데이터가 중요한 페이지 (검색 결과 페이지, 사용자 프로필)
CSR이 적합한 경우 (캐싱 안함)
Canvas, WebGL 등 브라우저 전용 API를 사용하는 페이지 (에디터, 차트 등)
SEO 중요도가 낮은 경우
PPR이 적합한 경우
동적인 부분과 정적인 부분을 함께 렌더링해야하는 페이지 (개인 위젯이 있는 홈 화면)
코드 실행 환경
Server Component
사용자 상호 작용이 없는 컴포넌트
Client Component
사용자 상호 작용이 있는 컴포넌트
dynamic + ssr:false
서버 환경에서 실행할 수 없는 API를 사용하는 컴포넌트
어떻게 정리가 좀 되셨나요? 여기에 더해 이제는 "use cache"를 활용하여 컴포넌트와 함수 단위로도 캐싱할 수 있다는 것을 아시면 될거 같아요.
NextJs가 업데이트 되면서 좋은 기능들이 많이 나오고 있어요. 그리고 실험단계인 기능들도 많고요. 그만큼 훨씬 체계적으로 관리할 수 있어서 좋은거 같아요. 하지만 그만큼 복잡해져서 과부하가 오고있다 생각해요. Github Next.js 레포에 가보면 Issue와 bug들이 감당할 수 없을 정도로 많아요. 새로운 기능들이 미완성인 상태로 나오고 있다고 판단되고 그만큼 사람들의 불만도 많아서 Remix로 옮겨가는 경우도 많아요.
이번 글의 주제인 cache component도 실제로 고려되지 않은 부분들이 많아서 제작자가 다시 설계해야할 것 같다고 언급했어요. 따라서 아직 PPR과 cache component 기능을 비즈니스에 사용하는 것은 절대 금지라고 생각해요. 전통적인 SSR Streaming 방식보다 PPR 방식이 사용자 경험이 더 좋은 것에 비해 버그가 너무 치명적이고 많기 때문이에요. 그리고 cache component로 migration 하더라도 설계가 바껴서 API가 바뀔지도 모른다고 생각해요.
저도 Next.js 16으로 옮기면서 버그를 해결 못한 상황이에요. Discussions로 도움 요청도 했지만 돌아오는 답변은 Dynamic Routing에서 'use cahce'가 버그가 있기 때문에 우선 다른 방식을 사용하는 것을 추천했어요. self-hosting하는 경우에는 제대로 작동하는데 vercel 서버에 올리면 캐싱이 작동하지 않는 버그였어요. 이런 고생 여러번 하다보면 괜히 최신 버전의 기술 스택을 적용하기 두려워지네요. ㅜㅜ