Zustand store가 복잡해질 때 상태 원본과 저장 범위를 어떻게 정하나요?
Zustand store가 복잡해지면 먼저 각 값의 원본과 저장 수명을 정합니다. API 작업을 Query 캐시와 store 양쪽에 복제하면 한쪽만 바뀌어 목록이 어긋날 수 있습니다.
적용 조건·범위: 자가 확인: 서버가 없는 브라우저 전용 Todo라면 tasks를 Zustand에 두는 선택도 가능할까요? 가능합니다. 판단 기준은 데이터 원본과 생명주기입니다.
이 기술만 따로 실행하기
설치 없이 메모장으로 진행하는 설계 실습입니다. 이전 프로젝트는 필요하지 않습니다.
서로 어긋나는 작업 목록의 원본 찾기
입력 상황: API 조회 결과를 Query 캐시와 Zustand의 tasks 양쪽에 저장했고, 저장 버튼은 API와 Query 캐시만 갱신합니다. UI 설정 compact와 임시 폼 문자열도 같은 store에 있습니다.
먼저 각 상태의 소유자와 이유를 직접 적은 뒤 다음 해설과 비교하세요.
분류 결과와 이유 확인
작업 수정 뒤 Zustand 목록이 낡을 수 있으므로 tasks의 읽기를 Query 캐시로 통일합니다. compact는 UI store와 persist 후보이고 임시 폼 문자열은 폼 로컬 state로 옮깁니다. 원본·사용처·초기화 시점의 세 열로 변경 계획을 작성합니다.
자가 확인: 서버가 없는 브라우저 전용 Todo라면 tasks를 Zustand에 두는 선택도 가능할까요? 가능합니다. 판단 기준은 데이터 원본과 생명주기입니다.
완료 기준: 상태마다 원본이 한 곳이고, 사용 범위·수명·저장 여부를 설명하는 분류표를 작성했습니다. 실제 앱 이전이나 렌더링 검증을 뜻하지 않습니다.
실수 1. 모든 상태를 Zustand에 넣기
다음 값은 한 컴포넌트에서만 사용한다면 로컬 state가 더 단순합니다.
inputValue
hoveredId
localErrorMessage
accordionOpen
임시 form field
const [title, setTitle] = useState('');
전역 store는 여러 위치에서 공유해야 하고 컴포넌트 하나의 생명주기에 묶기 어려운 상태에 집중합니다.
실수 2. 하나의 거대한 Store
useAppStore
├─ tasks
├─ theme
├─ modal
├─ sidebar
├─ notifications
├─ editor
└─ settings
다음 조건이 여러 개 겹치면 store 분리를 검토합니다.
- 변경 이유가 서로 다릅니다.
- 초기화 시점이 다릅니다.
- persist 정책이 다릅니다.
- 독립적으로 테스트할 필요가 있습니다.
- 서로 다른 화면에서 별개로 사용됩니다.
실수 3. 서버 상태 복제
서버에서 가져온 tasks를 TanStack Query와 Zustand에 동시에 저장하면 어느 쪽이 최신 원본인지 불분명해집니다.
서버 tasks, 로딩, 에러, 캐시, mutation
→ TanStack Query
selectedTaskId
taskFilter
task modal
UI preferences
→ Zustand
원본 데이터의 소유자를 하나로 유지하는 것이 중요합니다.
실수 4. setter만 늘리기
setTasks
setSelectedTaskId
setModalOpen
setPreferences
setter 자체는 문제가 아니지만 변경 규칙이 컴포넌트에 흩어질 수 있습니다. 행동 중심 action을 사용하면 책임이 더 명확합니다.
selectTask
openTaskModal
closeTaskModal
toggleCompact
실수 5. persist 범위가 너무 큼
persist되는 필드가 많을수록 migration 책임도 커집니다.
다음 질문으로 저장 여부를 결정합니다.
사용자가 다음 방문에도 기대하는가?
새로고침 후 복원할 가치가 있는가?
서버 원본과 중복되지 않는가?
구조 변경 비용을 감수할 가치가 있는가?
실수 6. selector 추상화가 과함
선택 훅은 반복을 줄일 수 있지만 모든 selector를 감추면 어떤 값을 구독하는지 추적하기 어려울 수 있습니다.
export const useTaskFilter = () =>
useTaskStore((state) => state.taskFilter);
의미가 반복되는 구독 단위만 추상화합니다.
Store 분리 예시
useTaskUiStore
├─ selectedTaskId
├─ taskFilter
├─ isTaskModalOpen
└─ actions
useUiPreferencesStore
├─ compact
├─ showCompleted
└─ persist
서버 tasks
→ TanStack Query
현재 TaskFlow 규모에서는 하나의 store를 유지해도 됩니다. 분리는 파일 수를 늘리는 목적이 아니라 책임을 분리하기 위한 선택입니다.
실무 점검 체크리스트
- 한 컴포넌트에서만 쓰는 state가 전역에 있지 않은가
- 서버 원본이 Zustand에 복제되지 않았는가
- action 이름이 행동을 설명하는가
- selector가 필요한 값만 구독하는가
- persist 대상이 최소화되어 있는가
- migration 기준이 있는가
- middleware가 실제 문제를 해결하는가
선택 실습: 할 일 앱에 적용하기
TaskFlow는 작업을 추가·완료·삭제하는 할 일 앱입니다. 이 선택 실습에서는 여러 화면이 공유하는 상태를 관리합니다.
최종 통합 전에 TaskFlow store의 상태 수명과 책임을 재검토합니다.
선택: 연재 프로젝트와 전체 자료
연재 프로젝트를 이어 만드는 분을 위한 자료입니다. 이 글의 개념과 예제만 찾으셨다면 필요한 파일과 실행 방법만 펼쳐보세요.
TaskFlow 프로젝트 적용
TaskFlow 직접 실행
docs/store-review.md로 각 상태의 수명·구독·저장 책임을 검토합니다. 설계 점검 단계라 앱 코드를 불필요하게 바꾸지 않습니다.
예제 크게 보기 (새 탭)중간부터 시작하는 전체 실습 파일
기존 React Context TaskFlow의 작업 목록·추가·완료·삭제 화면을 이어서 확장합니다.
이 글의 시작본은 직전 프로젝트 단계 완료본 전체입니다. 이미 직전 단계를 마쳤다면 그 폴더를 이어 쓰셔도 됩니다. Node.js 24.x와 npm을 사용하며 패키지 버전은 package-lock.json에 고정했습니다.
시작본에서 직접 바꾸기
시작본을 실행한 뒤 아래 변경 파일을 순서대로 수정합니다. 완료본의 같은 경로 파일에는 import와 입력 타입을 포함한 전체 코드가 있습니다.
docs/store-review.md— 이전 단계에 없던 파일을 만듭니다.
변경 이유와 결과: docs/store-review.md로 각 상태의 수명·구독·저장 책임을 검토합니다. 설계 점검 단계라 앱 코드를 불필요하게 바꾸지 않습니다.
완료본의 docs/changes.patch에서 시작·완료 차이를, docs/current-step.md에서 파일별 적용 위치를 확인할 수 있습니다. 아래 전체 파일을 같은 경로에 반영한 다음 실행하세요.
docs/store-review.md 전체 파일
# 최종 상태 점검
tasks는 메모리 원본이며 서버 원본의 복제본이 아닙니다. taskFilter·선택·표시 설정만 persist합니다. 모달과 입력은 저장하지 않습니다. 배열 selector에서 매번 새 배열을 반환하지 않고 tasks 원본과 필터를 따로 구독합니다. 서버 API를 도입하면 tasks 소유권을 Query로 옮기는 별도 트랙입니다.
완료 확인
- 시작본은 직전 완료본과 동일한 ZIP입니다. 압축을 푼 뒤
npm ci로 잠금 파일의 의존성을 설치합니다. npm run dev실행 후 터미널의 localhost 주소를 엽니다.- docs/store-review.md로 각 상태의 수명·구독·저장 책임을 검토합니다. 설계 점검 단계라 앱 코드를 불필요하게 바꾸지 않습니다.
npm run typecheck와npm run build가 성공하면 다음 프로젝트 단계로 갑니다.
이 체크포인트는 TypeScript 검사·프로덕션 빌드와 실제 소스를 사용한 JSDOM/상태 시나리오 검증을 통과했습니다. 별도 실제 브라우저 화면 검수는 이 파일 검증 범위에 포함하지 않았습니다.
아래 부분 예제는 핵심 변경을 설명합니다. 실행에 필요한 전체 파일과 보조 타입·API는 위 완료본에서 확인하세요.
누적 실습은 이 절의 프로젝트 단계 링크를 따라 진행합니다. 페이지 끝 학습 순서는 기술 주제 읽기 순서입니다.
이전 프로젝트 단계: Zustand persist 마이그레이션 기준: 저장된 상태 구조가 바뀔 때의 적용 결과에서 시작합니다. 아래 체크포인트 이름은 단계 식별자이며 다운로드 파일명이나 Git 명령이 아닙니다.
src/store/useTaskStore.ts의 필드별 사용처와 저장 정책을 대조합니다. 지금은 코드 길이만으로 store를 분리하지 말고 중복 원본·불필요 persist 항목부터 수정 후보로 기록합니다.
이전 체크포인트
zustand-step-13-migration
이번 목표
최종 통합 전에 TaskFlow store의 상태 수명과 책임을 재검토합니다.
상태 분류
tasks
→ 현재 학습용 로컬 데이터
→ API 도입 시 TanStack Query로 이동
taskFilter
→ Zustand
selectedTaskId
→ Zustand
isTaskModalOpen
→ Zustand
preferences
→ Zustand + persist
form title
→ TaskForm 로컬 state
파생 값은 저장하지 않기
다음 값은 store state로 보관하지 않습니다.
remainingCount
completedCount
visibleTasks
필요할 때 원본 state에서 계산합니다.
const completedCount = useTaskStore(
(state) =>
state.tasks.filter(
(task) => task.completed,
).length,
);
persist 재점검
저장:
taskFilter
selectedTaskId
preferences
저장하지 않음:
isTaskModalOpen
임시 form 값
loading / error
middleware 재점검
persist
→ 저장이 실제로 필요한 경우 유지
devtools
→ 개발 중 action 추적에 유용
subscribeWithSelector
→ 외부 구독이 실제로 있을 때 유지
immer
→ 중첩 상태 업데이트가 충분히 복잡할 때 유지
학습 과정에서 사용한 middleware를 실제 서비스에 전부 남길 필요는 없습니다.
실행 확인
npm run dev
npm run build
기존 기능, persist 범위, modal 초기화, selector 범위, 사용하지 않는 state와 middleware를 확인합니다.
현재 확장 단계
✓ 1~13. Zustand 주요 기능 학습
→ 14. 실무 설계 점검
○ 15. 최종 TaskFlow 통합
완료 체크포인트
zustand-step-14-design-review
다음 학습
마지막 글에서는 store, action, selector, persist, devtools, migration 기준을 하나의 TaskFlow 최종 구조로 통합합니다.
다음 프로젝트 단계: Zustand Todo 완성 실습: actions·선택 훅·persist 연결
공식 문서
- Zustand 공식 문서: https://zustand.docs.pmnd.rs/
- Guides: https://zustand.docs.pmnd.rs/guides/
글 소개·학습 준비·전체 목차

Zustand · 14 / 15 · 고급 학습 · 실무 설계 점검
Zustand는 작은 API 덕분에 쉽게 시작할 수 있지만, store에 상태가 계속 쌓이면 구조가 불분명해질 수 있습니다. 독립적인 서버 작업 목록 사례로 상태의 소유자와 저장 범위를 점검합니다. 하단에서는 이 기준을 TaskFlow store에 적용합니다.
이번 글에서 배우는 내용
- 모든 상태를 전역화하면 안 되는 이유
- 거대한 store를 피하는 기준
- 서버 상태와 클라이언트 상태 분리
- action 책임과 selector 범위
- persist 대상 최소화
- store 분리 기준
목차
이 글이 도움이 되었나요?
읽음 기록
읽음 — / 전체 15개 · 필수 14개
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.