로컬에서는 버튼을 누르면 즉각 반응했고, 데이터는 늘 최신이었는데 배포 후, 실제 사용자들이 들어오고 트래픽이 몰리는 프로덕션 환경을 마주했을 때가 되니 "코드가 에러 없이 돌아가는 것"과 "효율적으로 돌아가는 것"은 또 다른 단계라는 것을 알게 되었다.
데이터를 수정했는데도 화면은 요지부동이고, 반대로 불필요한 API 호출로 서버가 끙끙거리는 모습을 보고 나는 Next의 캐싱 시스템을 한번 파보기로 했다. 요근래 검색하며 깨달은 Next 캐싱 아키텍처란 무엇인지, 성능 최적화를 위해 고민했던 흔적들을 남겨보려 한다.
Next의 캐시가 그냥 하나의 큰 저장소인 줄 알았는데 막상 보니 Next는 목적과 수명이 다른 4가지 레이어를 가지고 있었다.
- Request Memoization: 렌더링 '한 번' 동안만 유효한 단기 기억 (함수 반환 값)
- Data Cache: 서버가 기억하는 영구적 데이터 (API 응답)
- Full Route Cache: 빌드 시점에 생성된 HTML과 RSC Payload (정적 렌더링)
- Router Cache: 브라우저가 기억하는 페이지 데이터 (클라이언트 측)
단계별로 동작하는 이 구조를 이해해야 Next에서 캐시를 적절하게 사용할 수 있다.
React를 처음 다룰 때 까다로웠던 것이 Props Drilling이었다. 상위 Layout에서 가져온 유저 정보를 저 깊숙한 컴포넌트까지 내려주기 위해 거쳐가는 과정들... 코드가 지저분해지는 게 싫었지만 그렇다고 각 컴포넌트에서 매번 fetch를 부르자니 API를 한 페이지에서 3~4번씩 호출해도 되나? 하는 우려가 있었다.
그런데 Next에서는 하나의 렌더링 패스 내에서 발생하는 중복된 fetch 요청은 자동으로 병합되는 Request Memoization이 있다.
이 덕분에 이제는 성능 걱정 없이 데이터가 필요한 컴포넌트에서 직접 데이터를 요청한다. 코드는 훨씬 깔끔해졌고, 실제 네트워크 요청은 딱 한 번만 나가게 되었다. (참고로 axios나 DB 직접 호출은 자동 적용이 안 돼서 React의 cache 함수로 감싸줘야 한다는 건 나중에야 알았다.)
타 기술 블로그 글을 참고하여 문제를 해결하려고 했는데 계속해서 에러가 발생했고,,한참 후에야 Next 버전 차이에서 때문임을 알게되었다.
14 버전 기반의 자료를 보고 데이터가 왜 갱신이 안 되는지? 머리 싸매며 revalidate 옵션을 만지작거렸는데, 15 버전으로 넘어오니 반대로 왜 매번 요청을 새로 보내지? 하는 상황이 벌어졌다.
이제는 데이터의 성격에 따라 전략을 명확히 나눈다. 변하지 않는 데이터는 force-cache로 박제하고, 실시간성이 중요한 데이터는 revalidateTag를 활용해 온디맨드로 갱신한다. 특히 쇼핑몰이나 게시판처럼 즉각적인 반응이 중요한 곳에서는 온디맨드 재검증이 필수다..!
서버 부하를 줄이는 최고의 방법은 미리 만들어 둔 걸 보여주는 것이다. Next는 빌드 타임에 페이지를 정적으로 만들려고 노력하고 이것을 Full Route Cache라고 한다.
하지만 나도 모르게 **동적 함수인 cookies(), headers(), searchParams 같은 것들로 이 정적 렌더링을 깨뜨리는 실수를 하고 있었다..
컴포넌트 내부 어디선가 무심코 쿠키를 조회하는 순간, Next는 이 페이지는 사용자마다 다르구나라고 판단해버리고 전체를 동적 렌더링으로 전환해버린다. 이걸 막기 위해 쿠키가 필요한 부분만 <Suspense>로 감싸거나 클라이언트 컴포넌트로 분리하는 '부분 렌더링' 전략을 썼다. 이 구조를 잡고 나니 그제야 내가 원하던 속도가 나왔다.
Next 15부터 페이지 이동 시 기본 캐시 시간. 즉, staleTime이 0초로 바뀌어 데이터 신선도 문제는 많이 해결되었다. 하지만 상품 목록처럼 뒤로 가기 경험이 중요한 곳에서는 설정을 통해 UX와 데이터 최신성 사이의 균형을 맞추는 것이 중요함을 알게되었다.
캐싱은 잘 쓰면 사용자에게 빠른 속도를 선물하고 서버 비용을 아껴주지만, 잘못 쓰면 사용자에게 엉뚱한 이전 데이터를 보여주는 치명적인 버그가 된다는 것을 잊지 말아야겠다..