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

7.0 KiB

id, title, category, status, canonical_id, aliases, duplicate_of, source_trust_level, confidence_score, verification_status, tags, raw_sources, last_reinforced, github_commit, tech_stack
id title category status canonical_id aliases duplicate_of source_trust_level confidence_score verification_status tags raw_sources last_reinforced github_commit tech_stack
wiki-2026-0508-zustand-based-mission-persistenc Zustand-Based Mission Persistence 10_Wiki/Topics verified self
Zustand Persist Pattern
Game Mission State
Long-running Task Store
none A 0.88 applied
zustand
state-management
persistence
react
game-state
2026-05-10 pending
language framework
TypeScript 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 정의

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

useEffect(() => {
  const unsub = useMissions.persist.onFinishHydration((s) => {
    Object.values(s.missions).forEach(m => {
      if (m.status === 'active') resumeMission(m);
    });
  });
  return unsub;
}, []);

Selector hooks

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)

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

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

🤖 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