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.
매 응용
RPG quest tracker (Zen-Pop 같은 calm UI 와 겹침).
Agent / LLM tool-use loop (Claude tool-use loop persistence).
Multi-step form / onboarding wizard.
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
Selector hooks
Server-sync (optimistic)
Devtools + immer
매 결정 기준
상황
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