--- id: wiki-2026-0508-zustand-based-mission-persistenc title: Zustand-Based Mission Persistence category: 10_Wiki/Topics status: verified canonical_id: self aliases: [Zustand Persist Pattern, Game Mission State, Long-running Task Store] duplicate_of: none source_trust_level: A confidence_score: 0.88 verification_status: applied tags: [zustand, state-management, persistence, react, game-state] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: TypeScript framework: Zustand 5 + IndexedDB / AsyncStorage --- # Zustand-Based Mission Persistence ## 매 한 줄 > **"매 long-running mission/quest state 를 Zustand store 에 normalized 하게 둔 다음, persist middleware 로 IndexedDB/AsyncStorage 에 매 incremental sync"**. 게임의 quest, agentic LLM workflow, multi-step onboarding 모두 동일 패턴. 매 3kb store + middleware 만으로 Redux + redux-persist 를 대체. ## 매 핵심 ### 매 Mission state 모델 - `missions: Record` (normalized). - `Mission = { id, status, steps[], currentStepIdx, payload, startedAt, updatedAt }`. - Active set 은 derived selector: `Object.values(missions).filter(m => m.status === 'active')`. ### 매 Persistence layer - **web**: `persist` middleware + custom IndexedDB storage (idb-keyval). - **RN**: persist + AsyncStorage / MMKV. - **partialize**: 매 transient state (UI loading, animation flags) 의 제외. - **version + migrate**: schema 변경 시 매 graceful upgrade. ### 매 Crash safety - Step 완료 직후 `set` → middleware 가 매 microtask 의 disk flush. - 매 atomic write — 매 mid-write crash 도 magic header 또는 swap-on-write 로 ok. - 매 startup 의 `hydrate()` 후 매 `inProgressStep` 의 resume. ### 매 응용 1. RPG quest tracker (Zen-Pop 같은 calm UI 와 겹침). 2. Agent / LLM tool-use loop (Claude tool-use loop persistence). 3. Multi-step form / onboarding wizard. 4. Offline-first todo / habit tracker. ## 💻 패턴 ### Store 정의 ```typescript import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; import { get as idbGet, set as idbSet, del as idbDel } from 'idb-keyval'; type Step = { id: string; status: 'pending' | 'running' | 'done' | 'failed'; output?: unknown }; type Mission = { id: string; title: string; status: 'active' | 'paused' | 'completed' | 'failed'; steps: Step[]; currentStepIdx: number; payload: Record; startedAt: number; updatedAt: number; }; interface State { missions: Record; start: (m: Omit) => void; advance: (id: string, output: unknown) => void; fail: (id: string, err: string) => void; complete: (id: string) => void; } const idbStorage = { getItem: (k: string) => idbGet(k).then(v => v ?? null), setItem: (k: string, v: string) => idbSet(k, v), removeItem: (k: string) => idbDel(k), }; export const useMissions = create()( persist( (set) => ({ missions: {}, start: (m) => set(s => ({ missions: { ...s.missions, [m.id]: { ...m, status: 'active', currentStepIdx: 0, startedAt: Date.now(), updatedAt: Date.now() } }, })), advance: (id, output) => set(s => { const m = s.missions[id]; if (!m) return s; const steps = m.steps.map((st, i) => i === m.currentStepIdx ? { ...st, status: 'done' as const, output } : st); const nextIdx = m.currentStepIdx + 1; const done = nextIdx >= steps.length; return { missions: { ...s.missions, [id]: { ...m, steps, currentStepIdx: done ? m.currentStepIdx : nextIdx, status: done ? 'completed' : 'active', updatedAt: Date.now(), } } }; }), fail: (id, err) => set(s => { const m = s.missions[id]; if (!m) return s; return { missions: { ...s.missions, [id]: { ...m, status: 'failed', payload: { ...m.payload, err }, updatedAt: Date.now() } } }; }), complete: (id) => set(s => ({ missions: { ...s.missions, [id]: { ...s.missions[id], status: 'completed', updatedAt: Date.now() } } })), }), { name: 'mission-store-v2', storage: createJSONStorage(() => idbStorage), version: 2, migrate: (state: any, from) => { if (from < 2) state.missions = state.missions ?? {}; return state; }, partialize: (s) => ({ missions: s.missions }), }, ), ); ``` ### Resume on app start ```typescript useEffect(() => { const unsub = useMissions.persist.onFinishHydration((s) => { Object.values(s.missions).forEach(m => { if (m.status === 'active') resumeMission(m); }); }); return unsub; }, []); ``` ### Selector hooks ```typescript export const useActiveMissions = () => useMissions(s => Object.values(s.missions).filter(m => m.status === 'active'), shallow); export const useMission = (id: string) => useMissions(s => s.missions[id]); ``` ### Server-sync (optimistic) ```typescript async function advanceWithSync(id: string, output: unknown) { useMissions.getState().advance(id, output); // local instant try { await fetch(`/api/missions/${id}/advance`, { method: 'POST', body: JSON.stringify({ output }) }); } catch { // 매 retry queue 에 push, persisted store 가 source of truth } } ``` ### Devtools + immer ```typescript import { devtools } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; create()(devtools(persist(immer((set) => ({ // immer 의 mutating syntax advance: (id, output) => set(s => { s.missions[id].steps[s.missions[id].currentStepIdx].status = 'done'; }), })), { name: 'missions' }))); ``` ## 매 결정 기준 | 상황 | Approach | |---|---| | Web (≤ 5MB state) | persist + IndexedDB (idb-keyval) | | RN (mobile) | persist + MMKV (≫ AsyncStorage perf) | | Cross-tab sync | persist + `storage` event 또는 BroadcastChannel | | Multi-user / cloud | local store + server sync layer (TanStack Query mutate) | | Real CRDT collab | Yjs + Zustand bridge | **기본값**: Zustand 5 + persist + idb-keyval + version/migrate + partialize + immer middleware. ## 🔗 Graph - 부모: [[Zustand]] - Adjacent: [[MMKV]] · [[Optimistic-UI]] ## 🤖 LLM 활용 **언제**: store scaffolding, migrate function, selector hook 도출. **언제 X**: 매 storage size / quota 결정, 매 multi-tab race condition debug — 매 empirical. ## ❌ 안티패턴 - **Persisting ephemeral UI flags**: 매 reload 시 stale loading spinner. Use partialize. - **No version field**: 매 schema 변경 시 매 user data loss. - **Direct localStorage on RN**: 매 size limit + sync. MMKV. - **Whole array re-create on each step**: 매 selector subscribers re-render. Use immer or fine-grained slicing. - **Storing secret tokens**: persisted store 의 plaintext leak. Use secure keychain. ## 🧪 검증 / 중복 - Verified (Zustand 5 docs, idb-keyval / MMKV docs, prod RPG/agent codebases). - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — Zustand persist + mission state pattern |