
캐시가 겹칠 때 벌어지는 일을 파헤치고, 1 줄로 해결한 경험담을 공유합니다.
Next.js의 ISR(Incremental Static Regeneration) 은 정적 페이지의 속도와 서버 렌더링의 실시간성을 모두 챙길 수 있는 멋진 기능입니다. 여기에 클라이언트 상태 관리 라이브러리인 TanStack Query (React Query) 를 더하면, 화면 전환은 더 빨라지고 네트워크 요청은 줄어듭니다.
그런데 두 캐시가 충돌하면 사용자가 방금 수정한 데이터조차 보이지 않는 불상사가 발생할 수 있습니다. 저의 레시피 공유 서비스에서 실제로 일어난 문제였죠. 글쓴이(레시피 작성자)는 “수정 완료” 메시지를 봤는데, 프로필 페이지엔 옛날 제목이 그대로 남아 있었습니다.
아래는 문제 원인부터 1 줄짜리 해결책, 그리고 얻은 교훈까지 정리한 기록입니다.
프로필 페이지(/profile/maria) — ISR로 24시간마다 재생성합니다.
Jamie가 레시피를 오전 10시에 수정합니다.
기대: 새로고침하면 즉시 수정된 제목이 보여야 합니다.
현실: 24시간이 지나 ISR이 다시 빌드되기 전까진 옛날 데이터가 그대로 노출됩니다.
방문자(maria가 아닌 사람)에게 캐싱된 페이지가 보이는 것은 좋았습니다. 그러나 작성자 본인이 바뀐 내용을 못 보는 UX는 치명적이죠.

초기 설정은 아래와 같았습니다.
// page.tsx
import {
dehydrate,
HydrationBoundary,
queryOptions,
} from '@tanstack/react-query';
export const revalidate = 86_400; // 24 h
async function ProfilePage({ params }: Props) {
const { username } = await params;
if (!username) return notFound();
const queryClient = getQueryClient();
queryClient.prefetchQuery(
queryOptions(
recipesOptions({
query: username,
}),
),
);
return(
<HydrationBoundary state={dehydrate(queryClient)}>
{...}
</HydrationBoundary>
);
}
// RecipeList.tsx
async function RecipeList() {
const query = useSuspenseQuery(recipesOptions({
query: params.username,
enabled: isUserProfile,
// 0 ms for owners → always refetch
// 24 h for visitors → performance
staleTime: isUserProfile ? 0 : 86_400_000,
}));
서버 프리패치 : Next.js가 페이지를 SSR/ISR 과정에서 렌더링할 때 서버에서 TanStack-Query prefetch를 호출해 데이터를 가져옵니다.
Hydration : 브라우저에서 TanStack-Query는 해당 상태를 그대로 복원(하이드레이션)합니다. 즉, 첫 마운트부터 캐시가 차 있습니다.
staleTime === 0은 데이터를 ‘오래됨(stale)’으로만 표시할 뿐, 메모리에서 삭제하지 않습니다. React Query는 백그라운드에서 리패칭을 시도할 수 있지만, 그 사이에는 이전 데이터를 그대로 보여줍니다.
작성자에겐 단 한 번의 오래된 렌더링도 UX 실패입니다.
gcTime(Garbage Collection Time)은 캐시 객체가 메모리에서 언제 완전히 사라질지 결정합니다. 0으로 설정하면 컴포넌트 언마운트 후(또는 stale 상태가 되자마자) 즉시 제거되며, 다음 마운트 시엔 빈 캐시로 시작해 네트워크 요청을 강제합니다.
recipesOptions({
query: params.username,
enabled: isUserProfile,
staleTime: isUserProfile ? 0 : 86_400_000,
gcTime: isUserProfile ? 0 : 86_400_000, // memory window
});
서버에서 가져온 cache된 데이터가 하이드레이션되지만, 곧바로 GC 대상이 됩니다.
React Query는 빈 캐시를 인식하고 즉시 리패칭합니다.
작성자는 최신 레시피를 단 한 번의 요청 후에 확인합니다.
캐시는 하루 동안 유지되며, 페이지 전환 시 즉시 렌더링됩니다.
서버 트래픽과 로딩 시간이 줄어듭니다.
두 캐시, 두 레버 — staleTime은 신선도, gcTime은 존속 기간을 조절합니다.
ISR이 주입한 초기 데이터를 클라이언트가 어떻게 다룰지 결정해야 ‘어제의 유령 데이터’를 피할 수 있습니다.
사용자 분기 캐싱(작성자 vs. 방문자)으로 UX와 성능을 모두 잡을 수 있습니다.
단 1 줄의 코드지만, 그 뒤엔 다층 캐시 흐름을 이해하는 사고 과정이 숨어 있습니다.
현대 프레임워크는 ‘빌드 타임’과 ‘런타임’의 경계를 흐립니다. 서버 캐시(ISR)와 클라이언트 캐시(React Query)가 겹칠 땐 데이터의 여정을 처음부터 끝까지 그려 보세요. 어느 지점에서 낡은 데이터가 끼어드는지 보인다면, 해결 레버(gcTime)가 저절로 눈에 들어올 것입니다.
Happy Caching!
Next.js ISR 공식 문서
- ISR — https://nextjs.org/docs/app/guides/incremental-static-regeneration
- Caching and Revalidating — https://nextjs.org/docs/app/getting-started/caching-and-revalidating
TanStack Query 공식 문서
- Prefetch - https://tanstack.com/query/latest/docs/framework/react/guides/prefetching
- Caching — https://tanstack.com/query/latest/docs/framework/react/guides/caching