Next.js 캐시와 TanStack Query 캐시 차이: 서버·클라이언트 경계 설계

··설명 약 12분·작성: 해비·블로그 소개

Next.js 서버 캐시와 TanStack Query 캐시는 어떻게 나누어 관리하나요?

브라우저의 TanStack Query 캐시와 Next.js 서버 캐시는 서로 다른 저장 공간입니다. 개인 목록은 queryKey와 invalidateQueries로, 서버에서 재사용하는 공개 데이터는 해당 서버 캐시의 재검증 경로로 다룹니다.

적용 조건·범위: 한 API 호출로 모든 캐시가 자동으로 서로 동기화된다고 가정하지 않습니다.

본문 근거: tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr · nextjs.org/docs/app/api-reference/directives/use-cache · nextjs.org/docs/app/api-reference/functions/revalidatePath

이 기술만 따로 연습하기

설치 없이 두 화면의 캐시 책임표를 작성합니다. 입력은 “모든 방문자에게 같은 공개 통계”와 “현재 로그인 사용자가 수정하는 작업 목록”입니다. 공개 통계는 Next.js 서버 캐시, 개인 목록은 브라우저 QueryClient를 사용한다고 가정합니다.

  1. 작업을 저장했을 때 개인 목록과 공개 통계 각각이 바뀌는지 표시합니다.
  2. 개인 목록에는 queryKey와 invalidateQueries, 공개 통계에는 실제 서버 캐시 모델의 재검증 경로를 적습니다.
  3. 새로고침 HTML만 오래된 경우와 현재 탭만 오래된 경우의 조사 대상을 다르게 적습니다.
관찰 결과 먼저 확인할 계층 확인할 연결
현재 탭 목록만 이전 값 브라우저 QueryClient mutation 성공 → 정확한 키 invalidation → GET 결과
서버가 만든 공개 통계 HTML이 이전 값 Next.js 서버 캐시 서버 캐시 사용 여부 → 저장 후 태그/경로 재검증
모달이 안 닫힘 UI state 모달을 제어하는 이벤트

기대 결과는 두 캐시를 한 저장소로 취급하지 않는 책임표입니다. 변형: 공개 통계를 서버에서 캐시하지 않는 앱이라면 그 데이터에 대한 서버 캐시 무효화 단계는 필요하지 않습니다. 이 설계 실습만으로 실제 Next.js 재검증을 실행한 것은 아닙니다.

TanStack Query cache

TanStack Query cache는 QueryClient가 관리합니다.

queryKey
queryFn 결과
staleTime
gcTime
mutation
invalidateQueries
setQueryData

브라우저에서 서버 데이터를 재사용하고, background refetch와 mutation 이후 동기화를 다루는 것이 핵심입니다.

Next.js 서버 캐시

Next.js 16에서 Cache Components를 활성화하면 async function이나 component에 use cache를 적용해 서버 결과를 캐시할 수 있습니다.

export async function getPublicTasks() {
  'use cache';


  return taskRepository.list();
}

cacheLife와 cacheTag를 함께 사용할 수도 있습니다.

import {
  cacheLife,
  cacheTag,
} from 'next/cache';


export async function getPublicTasks() {
  'use cache';


  cacheLife('minutes');
  cacheTag('tasks');


  return taskRepository.list();
}

이 cache는 QueryClient cache와 별개입니다.

Cache Components를 사용하지 않는 경우

현재 Next.js 문서의 previous model에서도 fetch 요청은 기본적으로 캐시되지 않습니다.

명시적으로 서버 fetch를 캐시하려면 다음처럼 설정할 수 있습니다.

fetch(url, {
  cache: 'force-cache',
});

또는 프로젝트 버전과 캐시 모델에 맞는 revalidation 설정을 사용합니다.

중요한 것은 “Next.js fetch는 항상 자동 캐시된다”라는 오래된 전제를 사용하지 않는 것입니다.

두 캐시 비교

Next.js server cache
- 서버 계층
- 여러 요청에서 재사용 가능
- Server Component / cached function 결과와 연결
- use cache, cacheLife, cacheTag
- revalidatePath / revalidateTag / updateTag 등


TanStack Query cache
- QueryClient 계층
- queryKey 기반 서버 상태
- 브라우저 상호작용에 강함
- staleTime / gcTime
- invalidateQueries / setQueryData
- mutation / optimistic update

invalidateQueries

브라우저 Query cache를 stale 처리하고 다시 서버 데이터를 확인하게 합니다.

queryClient.invalidateQueries({
  queryKey:
    taskKeys.lists(),
});

이 코드는 Next.js 서버 cache를 직접 무효화하지 않습니다.

revalidatePath

revalidatePath는 서버 환경에서 특정 Next.js path의 cached data를 무효화하는 API입니다.

import {
  revalidatePath,
} from 'next/cache';


revalidatePath('/tasks');

Client Component에서 호출하는 API가 아니며, QueryClient cache를 직접 바꾸지 않습니다.

양쪽 cache를 모두 사용하는 경우

예를 들어 공개 작업 통계는 Next.js 서버 cache를 사용하고, 로그인 사용자의 Task 목록은 TanStack Query가 관리한다고 가정할 수 있습니다.

mutation 후 어떤 데이터가 영향을 받는지에 따라 무효화 대상을 정합니다.

브라우저 Task 목록만 영향
→ invalidateQueries


서버에서 cache한 /tasks 결과도 영향
→ 서버 mutation 경로에서 revalidatePath / tag 처리
→ 브라우저에서는 invalidateQueries


둘 다 영향
→ 각 계층을 각각 무효화

한 API 호출로 모든 캐시가 자동으로 서로 동기화된다고 가정하지 않습니다.

중복 캐시를 줄이는 기준

같은 데이터에 여러 캐시 계층을 붙이면 정책도 늘어납니다.

다음 질문을 먼저 합니다.

  1. 이 데이터는 서버에서 여러 요청 간 재사용해야 하는가?
  2. 브라우저에서 refetch·mutation·optimistic update가 필요한가?
  3. SSR 초기 데이터만 필요하고 이후 상호작용은 적은가?
  4. 사용자별 민감 데이터인가?
  5. 캐시가 오래될 때 누가 무효화하는가?

추천 역할 패턴

서버 렌더 중심 공개 데이터

Server Component
+ Next.js use cache
+ 필요 시 revalidateTag/revalidatePath

Client Query가 필요하지 않을 수 있습니다.

사용자 상호작용이 많은 서버 데이터

TanStack Query
+ mutation
+ invalidateQueries
+ optimistic update

초기 SSR이 필요하면 Server prefetch + HydrationBoundary를 추가합니다.

클라이언트 UI 상태

Zustand 또는 React state

Query cache에 modal, selectedTaskId 같은 UI state를 넣지 않습니다.

서버 데이터와 UI 상태의 책임 구조

Next.js Route Handler / server data layer
│
├─ Server Component
│  ├─ 필요한 경우 server cache
│  ├─ QueryClient.query로 초기 데이터 준비
│  └─ dehydrate
│
├─ HydrationBoundary
│
└─ Client TaskFlow
   ├─ useQuery
   ├─ useMutation
   ├─ queryKey
   ├─ invalidateQueries
   ├─ optimistic update
   └─ rollback


클라이언트 UI 상태
→ Zustand / React state

mutation 이후 최종 흐름

사용자 작업 수정
→ useMutation
→ Route Handler / 서버 저장
→ 서버 캐시가 있다면 서버 측 revalidation
→ 브라우저 Query cache invalidation
→ 최신 데이터 refetch
→ UI 동기화

최종 학습 체크리스트

  • 서버 상태와 클라이언트 UI 상태를 구분할 수 있습니다.
  • QueryClientProvider와 useQuery를 구성할 수 있습니다.
  • queryFn과 queryKey를 설계할 수 있습니다.
  • staleTime과 gcTime을 구분할 수 있습니다.
  • useMutation과 invalidation을 연결할 수 있습니다.
  • pagination과 infinite query를 구현할 수 있습니다.
  • optimistic update와 rollback을 구현할 수 있습니다.
  • 목록·상세 cache 동기화 전략을 선택할 수 있습니다.
  • Server Component에서 Query cache를 준비할 수 있습니다.
  • dehydrate와 HydrationBoundary를 연결할 수 있습니다.
  • QueryClient 생명주기 오류를 진단할 수 있습니다.
  • Next.js 서버 cache와 TanStack Query cache의 책임을 구분할 수 있습니다.

선택 실습: 할 일 앱에 적용하기

TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 서버 데이터의 조회·변경과 캐시를 관리합니다.

TaskFlow의 캐시 소유권을 최종 문서화하고 중복 캐시 갱신 지점을 제거합니다.

선택: 연재 프로젝트와 전체 자료

연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.

TaskFlow 프로젝트 적용

중간부터 시작하는 전체 실습 파일

기존 Next.js Route Handler TaskFlow의 /tasks 화면과 /api/tasks를 이어서 확장합니다. 인증 없는 demo 메모리 저장소를 사용하는 로컬 실습이며 계정·외부 DB·API 키가 필요 없습니다. MSW를 활성화하지 않습니다. 서버를 다시 시작하면 데이터가 초기화됩니다.

이 글의 시작본은 직전 프로젝트 단계 완료본 전체입니다. 이미 직전 단계를 마쳤다면 그 폴더를 이어 쓰셔도 됩니다. Node.js 24.x와 npm을 사용하며 패키지 버전은 package-lock.json에 고정했습니다.

시작본에서 직접 바꾸기

시작본을 실행한 뒤 아래 변경 파일을 순서대로 수정합니다. 완료본의 같은 경로 파일에는 import와 입력 타입을 포함한 전체 코드가 있습니다.

  1. docs/cache-boundaries.md — 이전 단계에 없던 파일을 만듭니다.

변경 이유와 결과: docs/cache-boundaries.md로 서버 메모리 원본·Route Handler no-store·Query 캐시 책임과 갱신 경로를 정리합니다. 기존 실행 기능을 유지한 최종 결과입니다.

완료본의 docs/changes.patch에서 시작·완료 차이를, docs/current-step.md에서 파일별 적용 위치를 확인할 수 있습니다. 아래 전체 파일을 같은 경로에 반영한 다음 실행하세요.

docs/cache-boundaries.md 전체 파일

# 캐시 소유권 최종 점검

1. 서버 demo 메모리 저장소: Route Handler와 Server Component가 같은 listTasks 함수를 사용합니다. 영속 DB가 아닙니다.
2. /tasks는 force-dynamic이고 /api/tasks는 private, no-store입니다. 브라우저 HTTP 캐시를 Query 캐시로 오해하지 않습니다.
3. QueryClient: 서버는 요청마다 새로 생성, 브라우저는 재사용합니다. 서버 snapshot을 dehydrate한 뒤 HydrationBoundary로 전달합니다.
4. POST/PATCH 성공은 taskKeys.all 무효화. optimistic 실패는 저장한 목록·상세 snapshot으로 복원 후 서버 결과 재확인.
5. 이 예제는 인증 전 demo 트랙입니다. 실제 사용자 서비스로 확장할 때에는 사용자별 서버 권한·쿼리 경계·세션 변경 시 캐시 정리가 필요합니다.

완료 확인

  1. 시작본은 직전 완료본과 동일한 ZIP입니다. 압축을 푼 뒤 npm ci로 잠금 파일의 의존성을 설치합니다.
  2. npm run dev 실행 후 http://localhost:3000/tasks를 엽니다.
  3. docs/cache-boundaries.md로 서버 메모리 원본·Route Handler no-store·Query 캐시 책임과 갱신 경로를 정리합니다. 기존 실행 기능을 유지한 최종 결과입니다.
  4. npm run typecheck와 npm run build가 성공하면 다음 프로젝트 단계로 갑니다.

기존 체크포인트 검증 기록: TypeScript 검사·프로덕션 빌드와 실제 소스를 사용한 JSDOM/상태 시나리오 검증을 통과한 기록입니다. 별도 실제 브라우저 화면 검수는 그 파일 검증 범위에 포함하지 않았습니다. 이번 문장 보완에서 재실행한 결과는 아닙니다.

아래 부분 예제는 핵심 변경을 설명합니다. 실행에 필요한 전체 파일과 보조 타입·API는 위 완료본에서 확인하세요.

프로젝트 마지막 단계입니다. 누적 실습은 이 절의 프로젝트 단계 링크를 따라 진행합니다. 페이지 끝 학습 순서는 기술 주제 읽기 순서입니다.

이전 프로젝트 단계: TanStack Query Hydration 오류 해결: QueryClient 설정 기준. 위 시작본 ZIP 전체를 내려받으면 이전 글을 다시 조립하지 않고 이 단계부터 실행할 수 있습니다.

제공된 완료본의 docs/cache-boundaries.md에 저장 이후 영향받는 캐시와 갱신 책임을 기록합니다. 이 누적 샘플의 Route Handler는 no-store이므로 개념 예제의 서버 캐시 재검증을 무조건 추가하지 않습니다. 실제 서버 캐시를 도입한 데이터에만 그 갱신 경로를 따로 정합니다.

이전 체크포인트

query-step-14-hydration-fix

이번 목표

TaskFlow의 캐시 소유권을 최종 문서화하고 중복 캐시 갱신 지점을 제거합니다.

최종 책임표

GET /api/tasks 원본
→ 서버 데이터 계층


브라우저 tasks cache
→ TanStack Query


작업 저장/수정/삭제
→ useMutation + Route Handler


브라우저 목록 갱신
→ invalidateQueries


서버에서 명시적으로 cache한 결과
→ Next.js revalidation


selectedTaskId / modal / UI 설정
→ Zustand 또는 React state

불필요한 중복 제거

다음 구조는 피합니다.

TanStack Query
→ tasks


Zustand
→ 같은 tasks 복사


Server cache
→ 같은 사용자별 tasks를 별도 장기 보관


컴포넌트 state
→ 또 tasks 복사

각 데이터의 원본과 cache 책임을 명시합니다.

최종 검증

  1. 새로고침 시 SSR/Hydration이 정상인지 확인합니다.
  2. mutation 후 브라우저 Query cache가 갱신되는지 확인합니다.
  3. Next.js server cache를 사용하는 데이터라면 서버 revalidation도 확인합니다.
  4. selectedTaskId와 modal이 Query cache에 섞이지 않았는지 확인합니다.
  5. Network와 React Query Devtools에서 요청·cache 흐름을 확인합니다.
npm run build

완료 체크포인트

query-step-15-cache-boundary

이 체크포인트가 TanStack Query 과정의 최종 TaskFlow 서버 상태 관리 구조입니다.

다음 학습

TanStack Query 이후에는 프로젝트 요구에 따라 다음 방향으로 확장할 수 있습니다.

클라이언트 전역 UI 상태 심화
→ Zustand


Next.js Cache Components·Server Actions 심화
→ Next.js 고급


실제 백엔드 연동
→ Firebase / Supabase / 별도 API


복잡한 mutation 흐름
→ offline mutation·mutation queue·실시간 동기화

공식 문서

프로젝트 적용 후 확인: 저장 후 어느 캐시를 갱신해야 할까요?

관찰 절차 /tasks에서 작업을 저장하고 Route Handler/서버 캐시의 재검증 로그, 브라우저 Devtools의 목록 invalidation, Network GET 순서를 기록합니다. 새로고침한 HTML과 클라이언트 목록을 비교합니다.

예상 결과 서버 캐시를 실제 사용한다면 해당 재검증 후 새 요청에서 최신 값이 나오고, 브라우저 Query 캐시 무효화 뒤 활성 목록이 최신 응답을 반영합니다.

오류 점검 새로고침만 옛 값이면 서버 캐시와 재검증 경로를, 현재 탭만 옛 값이면 queryKey와 invalidation을 확인합니다. 캐시 계층을 사용하지 않는 곳에 불필요한 재검증을 가정하지 않습니다.

다음 학습 과정 완료 후 Zustand의 UI 상태 관리 또는 Next.js 서버 캐시 심화로 확장합니다.

기존 체크포인트 검증 범위 전체 체크포인트의 빌드·상태 동작은 로컬 검증을 통과했습니다. 실제 브라우저 시각 확인은 별도로 진행해야 합니다.

글 소개·학습 준비·전체 목차
Next.js 서버 캐시와 TanStack Query 클라이언트 캐시의 역할 구분을 표현한 표지

TanStack Query · 15 / 15 · 최종 설계 · Next.js와 Query 캐시 경계

Next.js App Router와 TanStack Query를 함께 사용하면 “둘 다 캐시를 제공하는데 무엇을 어디에 맡겨야 하는가?”라는 문제가 생깁니다.

핵심은 두 캐시가 같은 계층이 아니라는 점입니다.

TanStack Query는 브라우저 QueryClient가 queryKey 기준으로 서버 상태를 관리하고, Next.js 캐시는 서버 렌더링·데이터 계산 결과를 재사용하는 프레임워크 계층입니다.

현재 Next.js 16에서는 Cache Components가 도입되었고 cacheComponents를 활성화하면 use cache, cacheLife, cacheTag 같은 명시적 서버 캐싱 모델을 사용할 수 있습니다. Cache Components를 사용하지 않는 기존 모델에서도 fetch는 기본적으로 캐시되지 않으며 필요할 때 force-cache나 revalidation을 명시합니다.

이번 글에서 배우는 내용

  • TanStack Query cache의 소유 위치
  • Next.js 서버 캐시의 역할
  • Next.js 16 Cache Components
  • use cache와 Query cache 차이
  • invalidateQueries와 revalidatePath 차이
  • mutation 후 어느 캐시를 갱신해야 하는가
  • 서버 데이터와 UI 상태의 책임 경계

목차

이 글이 도움이 되었나요?

조회 중

읽음 기록

읽음 — / 전체 15개 · 필수 14개

이 글을 쓴 해비

웹사이트를 제작·운영하고, 개인 서비스와 자동화·AI 도구를 만듭니다. 직접 만들며 배운 내용과 문제 해결 과정을 BlogFlow에 기록합니다.

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기