Files
2nd/10_Wiki/Topics/Backend/Zustand-Based-Mission-Persistence.md
T
Antigravity Agent f8b21af4be Wiki cleanup: error-doc removal, dedup merge, link normalization
10_Wiki/Topics 대규모 정리:
- 오류 캡처/미완성 stub 문서 227개 제거
- 교차폴더 중복 43클러스터 병합 (63파일 → redirect)
- 링크명 정규화: 깨진 링크 수정·redirect 직결·개념 매핑 ~2,400건
- 카테고리 MOC 6개 신규 생성
- Graph 섹션 미해결 related-keyword 링크 10,058건 제거

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 23:52:15 +09:00

199 lines
7.0 KiB
Markdown

---
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<MissionId, Mission>` (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<string, unknown>;
startedAt: number;
updatedAt: number;
};
interface State {
missions: Record<string, Mission>;
start: (m: Omit<Mission, 'startedAt' | 'updatedAt' | 'currentStepIdx' | 'status'>) => 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<State>()(
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<State>()(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 |