TanStack Query vs Zustand: 서버 상태와 클라이언트 상태 차이

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

TanStack Query와 Zustand는 서버 상태와 클라이언트 상태를 어떻게 나누어 맡나요?

서버가 원본인 목록의 조회·캐시는 TanStack Query, 브라우저의 공유 UI는 Context나 Zustand에 맡길 수 있습니다. 제목 입력·선택 ID·모달의 소유자를 나누어 같은 데이터를 두 저장소에 복제하지 않는 기준을 잡습니다.

적용 조건·범위: 이 과정의 기존 예제는 TanStack Query v5 객체 문법을 기준으로 합니다. 실제 설치와 실행에서는 각 실습의 package-lock.json에 고정된 버전을 사용합니다.

본문 근거: tanstack.com/query/latest · tanstack.com/query/latest/docs/framework/react/quick-start

이 기술만 따로 연습하기

별도 앱이나 계정 없이 “회원이 할 일을 조회하고, 제목을 입력하고, 편집 모달을 여는 화면”을 설계합니다. 입력 데이터는 서버에서 받은 작업 배열, 편집 중인 제목, 여러 화면이 공유하는 선택 ID, 모달 열림 여부 네 가지입니다.

  1. 각 데이터의 원본이 서버인지 현재 브라우저인지 적습니다.
  2. 한 컴포넌트 안에서만 쓰는지 여러 화면이 함께 쓰는지 표시합니다.
  3. 저장 성공 시 다시 받아야 할 값과 모달을 닫아도 유지할 값을 구분합니다.
데이터 예시 소유자 갱신 계기
서버 작업 배열 TanStack Query 저장 성공 후 재검증
편집 중인 제목 React local state 입력 이벤트
여러 화면의 선택 ID Zustand 등 공유 UI state 선택 이벤트
한 폼의 모달 표시 React local state 열기·닫기

기대 결과는 위와 같은 상태 소유권 표입니다. Query 데이터를 다시 Zustand에 복사하는 두 번째 원본을 만들지 않습니다. 변형: 모달을 여러 화면이 공유한다면 모달 state의 범위만 넓히면 되며, 작업 목록의 소유자까지 바꿀 필요는 없습니다.

서버 상태란 무엇인가

서버 상태는 브라우저가 소유하는 값이 아니라 서버나 API가 원본을 가지고 있는 데이터입니다.

예를 들어 TaskFlow의 작업 목록을 다음 API에서 가져온다고 가정합니다.

GET /api/tasks

이 데이터에는 단순한 값 저장 이상의 문제가 따라옵니다.

  • 언제 요청해야 하는가
  • 요청 중인가
  • 실패했는가
  • 이전 결과를 재사용할 수 있는가
  • 언제 다시 가져와야 하는가
  • 저장 후 어떤 목록을 갱신해야 하는가
  • 여러 컴포넌트가 같은 요청을 사용할 때 어떻게 캐시할 것인가

TanStack Query는 이런 서버 상태의 조회·캐시·동기화를 관리합니다.

Zustand가 맡는 상태

Zustand는 서버 원본을 저장하는 도구라기보다 클라이언트 전역 상태를 관리하는 데 적합합니다.

selectedTaskId
isTaskModalOpen
sidebarOpen
사용자 UI 설정
클라이언트 필터 상태

이 값들은 서버에서 가져온 원본 데이터가 아니라 UI 동작과 선택 상태입니다.

React state가 맡는 상태

한 컴포넌트 안에서만 사용하는 값은 로컬 state가 더 단순합니다.

const [title, setTitle] = useState('');

TaskForm의 입력 중인 문자열처럼 다른 화면이 알 필요가 없는 값은 굳이 TanStack Query나 Zustand에 넣지 않습니다.

역할을 한 줄로 구분하기

API 원본 데이터와 서버 동기화
→ TanStack Query


여러 컴포넌트가 공유하는 클라이언트 UI 상태
→ Zustand


한 컴포넌트 내부의 임시 상태
→ React state

Query 캐시와 Next.js 캐시는 다릅니다

TanStack Query의 QueryClient 캐시는 클라이언트 애플리케이션이 queryKey를 기준으로 서버 상태를 관리하는 캐시입니다.

Next.js의 서버 측 캐싱은 Server Component, fetch, Cache Components, revalidation 등 서버 렌더링 계층의 문제입니다.

둘 다 “캐시”라는 단어를 사용하지만 같은 저장소가 아닙니다.

이 차이는 마지막 15회차에서 다시 정리합니다.

TanStack Query 학습 순서

서버 상태 경계 결정
→ TanStack Query 설치
→ QueryClientProvider
→ 첫 useQuery
→ queryFn 분리
→ queryKey 설계
→ staleTime / gcTime
→ useMutation
→ invalidateQueries / setQueryData
→ pagination
→ useInfiniteQuery
→ optimistic update / rollback
→ 목록·상세 캐시 동기화
→ 서버 prefetch
→ HydrationBoundary
→ Next.js cache와 Query cache 경계

이 과정의 기존 예제는 TanStack Query v5 객체 문법을 기준으로 합니다. 실제 설치와 실행에서는 각 실습의 package-lock.json에 고정된 버전을 사용합니다.

자주 하는 실수

API 응답을 Zustand에 다시 복사합니다

TanStack Query cache
→ tasks


Zustand
→ 같은 tasks 복사

이렇게 두 원본을 만들면 mutation 이후 어느 값을 갱신해야 하는지 불명확해집니다.

모든 fetch를 TanStack Query로 바꿉니다

Server Component에서 서버 렌더링만 필요한 데이터까지 반드시 useQuery로 바꿀 필요는 없습니다.

TanStack Query는 클라이언트에서 서버 상태를 캐시·재요청·변경해야 할 때 특히 유용합니다.

loading을 별도 useState로 다시 만듭니다

useQuery가 이미 pending/error/success 상태를 제공합니다. 같은 상태를 별도로 복제하지 않습니다.

학습 완료 체크리스트

  • 서버 상태와 클라이언트 상태를 구분할 수 있습니다.
  • TanStack Query와 Zustand의 역할을 설명할 수 있습니다.
  • Query 캐시와 Next.js 서버 캐시가 같은 개념이 아님을 이해했습니다.
  • 서버가 원본인 목록과 브라우저가 원본인 UI 상태의 소유자를 구분할 수 있습니다.

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

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

코드를 크게 바꾸지 않고 상태 소유권부터 정합니다.

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

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

TaskFlow 프로젝트 적용

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

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

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

시작본에서 직접 바꾸기

개념·경계 설계 단계라 실행 앱을 바꾸지 않고 ZIP의 문서를 직접 작성·비교합니다.

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

변경 이유와 결과: docs/state-ownership.md에 서버 tasks와 UI 선택·로컬 입력을 구분합니다. 기존 실행 앱은 유지합니다.

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

docs/state-ownership.md 전체 파일

# 원본 소유권

서버의 /api/tasks가 tasks 원본입니다. 클라이언트 응답 캐시는 Query가 관리합니다. 선택 ID·필터 UI는 React state, 입력 문자열은 TaskForm입니다. Zustand에 같은 tasks를 다시 복사하지 않습니다.

완료 확인

  1. 시작본은 직전 완료본과 동일한 ZIP입니다. 압축을 푼 뒤 npm ci로 잠금 파일의 의존성을 설치합니다.
  2. npm run dev 실행 후 http://localhost:3000/tasks를 엽니다.
  3. docs/state-ownership.md에 서버 tasks와 UI 선택·로컬 입력을 구분합니다. 기존 실행 앱은 유지합니다.
  4. npm run typecheck와 npm run build가 성공하면 다음 프로젝트 단계로 갑니다.

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

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

연속 학습의 이전 글: Next.js Route Handler 405 오류 해결. 위 시작본 ZIP 전체를 내려받으면 이전 글을 다시 조립하지 않고 이 단계부터 실행할 수 있습니다.

기반 체크포인트는 다음과 같습니다.

next-step-17-route-handler

docs/state-ownership.md에 API 목록·공유 UI·로컬 입력의 소유자를 기록합니다. Route Handler와 클라이언트 화면을 수정하기 전에 중복 원본을 없애는 설계입니다.

이전 체크포인트

next-step-17-route-handler

현재 상태

TaskFlow에는 Route Handler가 준비되어 있습니다.

GET /api/tasks
POST /api/tasks
Server Component / Client Component 경계

이번 목표

코드를 크게 바꾸지 않고 상태 소유권부터 정합니다.

/tasks API 원본
→ TanStack Query


selectedTaskId
modal
UI 설정
→ Zustand 또는 React state


TaskForm 입력 문자열
→ React local state

Before

fetch 결과와 UI 상태의 책임이 명확히 분리되지 않음

After

서버 원본 / 클라이언트 전역 / 로컬 상태
세 영역의 소유자를 먼저 결정

현재 확장 단계

→ 1. 서버 상태 경계
○ 2. Provider + useQuery
○ 3. queryFn
○ 4. queryKey
○ 5. 캐시 수명
○ 6~11. mutation·동기화·복구
○ 12~15. SSR·Hydration·캐시 경계

완료 체크포인트

query-step-01-boundary

다음 학습

다음 글에서는 TanStack Query를 실제 설치하고 QueryClientProvider와 첫 useQuery를 연결합니다.

다음 글: TanStack Query Provider와 첫 useQuery 상태 처리 실습 가이드

공식 문서

프로젝트 적용 후 확인: 서버 상태와 UI 상태를 어디에 둘까요?

관찰 절차 TaskFlow에서 /tasks를 열고 Network의 GET /api/tasks를 확인합니다. 서버의 작업 제목을 바꾼 뒤 목록 요청을 다시 받아 화면 값이 서버 응답을 따라가는지 봅니다. 선택한 작업 ID와 모달 열림 값은 React state 또는 Zustand에 두고, 모달만 열고 닫을 때 GET 요청이 추가되지 않는지도 비교합니다.

예상 결과 서버 목록은 응답과 동기화되고 모달 상태 변경은 목록 요청과 무관합니다.

오류 점검 작업 제목이 변하지 않으면 서버 응답, queryKey, staleTime과 refetch 조건을 순서대로 확인합니다.

다음 학습 2회차 Provider와 첫 useQuery에서 실제 조회 상태를 연결합니다.

기존 검증 기록 원문에는 전체 체크포인트의 빌드·상태 동작이 로컬 검증을 통과한 것으로 기록되어 있습니다. 실제 브라우저 시각 확인은 별도로 진행해야 합니다.

글 소개·학습 준비·전체 목차
TanStack Query vs Zustand: 서버 상태와 클라이언트 상태 차이

TanStack Query · 1 / 15 · 필수 길잡이 · 서버 상태 경계

TanStack Query를 처음 배울 때 가장 중요한 것은 useQuery 문법을 외우는 것이 아니라, 어떤 데이터를 TanStack Query가 맡아야 하는지 구분하는 것입니다.

상단은 서버 데이터와 UI 상태의 소유권을 정하는 독립 설계 실습입니다. 하단의 누적 프로젝트 경로는 Next.js TaskFlow의 Route Handler를 기반으로 조회·캐시·변경·SSR/Hydration을 확장합니다.

상단에서는 이 글만 찾아온 독자도 TanStack Query와 Zustand의 역할 차이를 이해할 수 있게 설명하고, 하단에서는 TaskFlow의 서버 상태 경계를 정리합니다.

이번 글에서 배우는 내용

  • 서버 상태와 클라이언트 상태의 차이
  • TanStack Query가 해결하는 문제
  • Zustand가 맡는 상태
  • React 로컬 state가 맡는 상태
  • Next.js 서버 캐시와 Query 캐시가 다른 이유
  • TaskFlow 15회 학습 흐름

먼저 알아야 할 내용

상단의 상태 소유권 실습은 설치 없이 진행하며 React 컴포넌트와 state의 역할만 알면 됩니다. 아래 기술 목록 중 Next.js App Router·서버/클라이언트 컴포넌트·Route Handler는 하단 누적 프로젝트를 이어 만들 때 필요한 지식입니다.

  • React 컴포넌트와 state
  • Next.js App Router
  • Server Component와 Client Component
  • Route Handler
  • fetch 기본 사용법
  • TypeScript 기본 타입

목차

이 글이 도움이 되었나요?

조회 중

읽음 기록

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

이 글을 쓴 해비

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

새 글 받아보기

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

RSS 피드 구독하기

댓글 남기기