Skip to content

ADR-26: Zustand 클라이언트 상태 관리

🇺🇸 English Version

날짜작성자리포지토리
2026-01-23@KubrickCodeweb

Context

Specvital 웹 플랫폼에 기존 상태 관리 솔루션과 다른 임시 클라이언트 사이드 상태 필요:

상태 카테고리관리 도구예시
서버 상태TanStack Query리포지토리 데이터, 분석 결과, 사용자 프로필
URL 상태nuqs대시보드 필터, 검색 쿼리, 뷰 모드
클라이언트 상태필요백그라운드 작업 추적, 세션 임시 데이터

문제

모달 기반 UI의 백그라운드 작업(분석, 스펙 생성)에 치명적 한계 존재:

  1. 모달 닫을 시 상태 손실
  2. 페이지 네비게이션 시 폴링 중단
  3. 실행 중인 작업의 페이지 간 가시성 부재

Issue #240 사용자 의견: "모달을 닫거나 다른 페이지로 이동하면 스펙 생성 진행 상황 추적 불가."

제약 조건

  • App Router RSC 경계: 상태 관리는 클라이언트/서버 컴포넌트 경계 준수 필요
  • Hydration 불일치 위험: SSR + 클라이언트 상태 조합 시 신중한 처리 필요
  • 번들 크기 민감성: 대시보드 성능 중요
  • 기존 스택 통합: TanStack Query, nuqs와 공존 필요

Decision

클라이언트 사이드 전역 상태 관리를 위해 Zustand 도입.

근거

  1. 상태 관리 3종 세트 완성: TanStack Query (서버), nuqs (URL), Zustand (클라이언트)
  2. 최소 번들 영향 (~3 KB gzipped)
  3. Provider 래퍼 불필요
  4. 내장 미들웨어: persist, devtools
  5. React 18+ 최적화를 위한 useSyncExternalStore 네이티브 사용
  6. 완전한 타입 추론의 TypeScript 우선 설계

Options Considered

Option A: Zustand (선택됨)

특성:

  • ~3 KB gzipped 번들 크기
  • Provider 래퍼 불필요
  • 선택적 구독이 가능한 Hooks 기반 API

장점:

  • 보일러플레이트 제로, 즉각적 생산성
  • 셋업 없이 컴포넌트 트리 어디서나 동작
  • Redux DevTools 호환
  • 스토리지 동기화를 위한 내장 persist 미들웨어

단점:

  • 대규모 코드베이스에서 Redux보다 구조화 부족
  • 팀 컨벤션 수립 필요 (강제 패턴 없음)

Option B: Redux Toolkit

특성:

  • ~10-15 KB gzipped
  • Provider 래퍼 필수
  • 서버 상태용 RTK Query

평가: 기각. RTK Query가 기존 TanStack Query와 중복. 남은 클라이언트 상태 요구사항에 비해 추가 보일러플레이트와 Provider 요구사항 정당화 불가.

Option C: React Context + useReducer

특성:

  • 0 KB (내장)
  • Provider 래퍼 필수
  • 수동 최적화 필요

평가: 기각. 자주 변경되는 상태에서 리렌더링 문제 발생. Context는 저빈도 업데이트용 설계, 작업 추적 같은 동적 상태에 부적합.

Option D: Jotai (Atomic State)

특성:

  • ~3.5 KB gzipped
  • Atom 기반 구성
  • Provider 선택적 (권장)

평가: 적합하나 미선택. Atom 기반 모델은 다른 사고 모델 필요. Zustand의 스토어 기반 접근 방식이 단순한 작업 추적 요구에 더 직관적.

Implementation

Store 구조

typescript
type TaskStoreState = {
  tasks: Map<string, BackgroundTask>;
};

type TaskStoreActions = {
  addTask: (task: Omit<BackgroundTask, "createdAt">) => void;
  updateTask: (id: string, updates: Partial<BackgroundTask>) => void;
  removeTask: (id: string) => void;
  clearCompletedTasks: () => void;
};

주요 파일

파일용도
lib/background-tasks/task-store.ts메인 Zustand 스토어
lib/background-tasks/hooks.ts소비자용 커스텀 훅
lib/background-tasks/task-store.spec.ts유닛 테스트
lib/background-tasks/components/task-badge.tsx활성 작업 수 표시 뱃지

사용 패턴

패턴구현
싱글톤 Storecreate()로 단일 useTaskStore 생성
파생 SelectorsuseActiveTasks로 필터링된 결과 계산
Shallow 비교useShallow로 불필요한 리렌더링 방지
React 외부 접근비컴포넌트에서 useTaskStore.getState() 사용
수동 영속화persist 미들웨어 대신 커스텀 sessionStorage 동기화

영속화

Zustand의 persist 미들웨어 대신 커스텀 sessionStorage 구현:

  • 스토리지 키: specvital:background-tasks
  • typeof window === "undefined" 체크로 SSR 안전
  • 손상된 스토리지나 쿼터 초과 시 조용한 오류 처리

Consequences

긍정적

  • 최소 통합 마찰: Provider 불필요, 즉시 동작
  • 개발자 경험: 상태 + 액션이 동일 파일에 위치한 단일 파일 스토어
  • 성능: 선택적 구독으로 불필요한 리렌더링 방지
  • 디버깅: Redux DevTools 호환

부정적

  • 팀 일관성 리스크: 강제 패턴 없음
    • 완화책: 스토어 구성 컨벤션 문서화, 코드 리뷰로 강제
  • 제한된 미들웨어 생태계: Redux보다 적은 커뮤니티 미들웨어
    • 완화책: 핵심 요구사항(persist, devtools) 충족됨
  • Next.js SSR 고려사항: 스토어가 모듈 레벨
    • 완화책: 공식 Zustand Next.js 가이드 준수

References

내부 문서

외부 문서

관련 커밋

  • 8664cbc: feat(background-tasks): add global task store with persistence
  • ecb4434: feat(background-tasks): integrate Account Badge and Tasks Dropdown
  • fc99ce5: feat(background-tasks): add Dashboard active tasks section
  • 2e1e6df: refactor(dashboard): migrate use-reanalyze hook to TanStack Query polling

Open-source test coverage insights