영속화 마이그레이션
이번 영속화 개편은 호환성을 깨지만 patch로 출시하는 API 변경입니다. 이슈 #102의 명시적 v2 수정 결정에 따른 예외이며 일반적인 semver 정책 변경은 아닙니다.
이전 API와 새 API
| 이전 | 이후 |
|---|---|
{ local: key, decode } | { key, storage: jsonStorage(() => window.localStorage), decode } |
{ session: key, decode } | { key, storage: jsonStorage(() => window.sessionStorage), decode } |
{ cookie: key, decode } | { key, storage: cookieStorage({ path: '/' }), decode } |
| 파일을 JSON으로 변환 | storage: indexedDBStorage({ database: 'my-app-state' }) |
자동 복원 / skipHydration: true | 생성 시 I/O 없음. 명시적 await store.persist.rehydrate() |
hasHydrated() | getStatus().hydration === 'hydrated' |
onRehydrateStorage | subscribe(listener)와 거부된 작업 처리 |
| 동기 복원과 삭제 | await rehydrate()와 await clearStorage() |
| commit 즉시 저장 완료로 가정 | 저장 완료 표시나 정리 전에 await flush() |
어댑터는 @ilokesto/state/middleware에서 가져옵니다. 각 헬퍼는 이미 팩토리를 반환하므로 추가 () =>로 감싸지 마세요. jsonStorage만 getter를 받아 활성화 전까지 브라우저 전역을 평가하지 않습니다.
기존 JSON 스토어 이전
기존 { state, version } 데이터를 읽으려면 같은 저장소와 키를 유지하세요. 쿠키의 기존 인코딩도 유지합니다. IndexedDB로 바꿔도 다른 저장소의 데이터를 자동으로 복사하지는 않습니다.
다음 완전한 브라우저 모듈 예제는 기존 settings 키를 읽고 migration 후 검증하며 migration 결과 저장 완료까지 기다립니다.
import { dispose, jsonStorage, persist } from '@ilokesto/state/middleware';
import type { PersistMigration } from '@ilokesto/state/middleware';
import { pipe } from '@ilokesto/state/utils';
type SettingsV1 = { theme: string };
type SettingsState = { theme: string; count: number };
const toV1: PersistMigration<unknown, SettingsV1> = (old) => ({
theme: typeof old === 'string' ? old : 'light',
});
const toCurrent: PersistMigration<SettingsV1, SettingsState> = (old) => ({
...old,
count: 0,
});
const decodeSettings = (value: unknown): SettingsState | null => {
if (typeof value !== 'object' || value === null) return null;
if (!('theme' in value) || typeof value.theme !== 'string') return null;
if (!('count' in value) || typeof value.count !== 'number') return null;
return { theme: value.theme, count: value.count };
};
const settingsStore = pipe
.use(persist({
key: 'settings',
storage: jsonStorage(() => window.localStorage),
migrate: [toV1, toCurrent],
decode: decodeSettings,
}))
.create<SettingsState>({ theme: 'light', count: 0 });
try {
await settingsStore.persist.rehydrate();
await settingsStore.persist.flush();
} finally {
dispose(settingsStore);
}이제 sessionStorage를 포함한 모든 어댑터가 같은 migration 파이프라인을 사용합니다. 저장된 버전 인덱스부터 남은 배열을 순서대로 실행한 뒤 decode와 등록된 상태 검증을 거쳐 복원 commit을 적용합니다. 데이터 버전은 migration 배열 길이이며 IndexedDB 스키마 version, 스토어 내부 commit 순번과 구분합니다.
잘못된 저장 형식, 미래 데이터 버전, migration 실패, decode 거절은 현재 상태와 저장값을 보존하고 복원을 거부합니다. 성공한 migration은 다시 쓰기를 예약하므로 저장 완료가 필요하면 flush()를 기다리세요. 현재 버전의 데이터는 다시 쓰지 않고 decode합니다.
복원 전 편집 충돌 해결
기본적으로 복원 완료 전 편집은 PersistError.code === 'CONFLICT'로 거부되며 메모리와 저장소를 그대로 유지합니다. 사용자에게 두 가지 명시적 선택을 제공하세요.
await store.persist.rehydrate({ conflict: 'keep-current' })는 현재 편집을 유지하고 저장을 예약합니다.await store.persist.rehydrate({ conflict: 'use-stored' })는 대기 중인 현재 편집을 버리고 검증한 저장값을 적용합니다.
깊은 병합은 없습니다. 복원 성공 전 flush()는 NOT_HYDRATED로 거부되므로 읽지 않은 데이터를 조용히 덮어쓰지 않습니다.
빌드 후 워크스페이스 루트에서 다음 예제를 실행하세요.
node packages/state/examples/persist-lifecycle.mjs메타데이터 없는 비동기 저장소 팩토리로 저장한 카운터를 복원하고, 두 충돌 정책을 실행하며, 메모리를 유지한 채 해당 키만 삭제하고 독립적으로 소유한 인스턴스를 정리합니다. 타이머나 sleep 없이 결과 값을 검증합니다.
파일, 완료, 정리
- IndexedDB는 structured clone으로
File,Blob,Map,Set,Date,ArrayBuffer를 보존합니다. base64 인코딩 없이 파일 이름, MIME 타입, 내용이 유지됩니다. - JSON 어댑터는 JSON으로 표현 가능한 값만 지원합니다.
Date.toJSON()은 문자열이 되며Map,Set,Blob등 지원하지 않는 값은 내용을 조용히 버리는 대신 거부합니다. IndexedDB에서 localStorage로 자동 전환하지 않습니다. - 복원한 Blob의 미리보기
blob:URL은 UI에서 생성하고 해제하며 URL 자체는 저장하지 않습니다. - IndexedDB 저장 성공은 request 성공이 아니라 transaction 완료 기준입니다. 복제, 용량, 트랜잭션 오류는 명시적인 실패로 남습니다.
flush()는 미래 편집이 아닌 호출 시점의 저장 대상을 기다리고 상태 debounce 타이머를 강제로 실행하지 않습니다. 실패한 쓰기는 대기 상태를 유지하며 새 commit이나flush()로 재시도합니다.clearStorage()는 이전 쓰기 뒤에 해당 키만 삭제하고 메모리를 유지합니다. 삭제로 대체된 대기 중flush()는CLEARED로 거부됩니다.dispose(store)는 해당 스토어의 어댑터와 대기 작업만 소유합니다. 팩토리를 재사용해도 연결과 정리 소유권은 공유하지 않습니다. 정리 전 저장 보장이 필요하면flush()를 먼저 기다리세요. 정리는 완료된 트랜잭션을 되돌리지 않습니다.- 복원은 검증 후 즉시 commit되며 새 history 기준점이 됩니다.
history()와debounce()또는throttle()의 조합 제한은 유지됩니다.
상태, 오류, 어댑터, SSR 계약은 persist를 참고하세요.