docs(10_Wiki): Topic_Business/General/Graphic/Programming을 Topics/ 하위로 이동
최상위 10_Wiki/Topic_*였던 4개 카테고리 폴더를 10_Wiki/Topics/Topic_* 로 재배치. 콘텐츠 변경 없음(순수 폴더 이동) — Topics/ 하위 나머지 폴더는 이미 지난 커밋에서 전부 정리된 상태(잔존 항목은 에이전트 운영 상태 및 사용자가 보존을 요청한 업데이트0615/무제 3.canvas 뿐).
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
---
|
||||
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 |
|
||||
Reference in New Issue
Block a user