React Native에서 버전 관리된 영속 상태를 위한 간단한 패턴

발행: (2026년 1월 16일 오전 08:40 GMT+9)
7 min read
원문: Dev.to

Source: Dev.to

내가 겪은 문제 (매우 빠르게)

첫 번째 React Native 프로젝트에서 AsyncStorage를 사용해 사용자 설정을 영구 저장해야 했습니다. 처음에는 간단했습니다:

await AsyncStorage.setItem("state", JSON.stringify(data));

하지만 프로젝트가 커지면서 여러 문제가 발생했습니다:

  • 새로운 필드를 추가함
  • 다른 필드의 이름을 바꿈
  • 기존 사용자에 대한 기본값이 필요함
  • TypeScript가 실제로 도움이 되길 원함
  • 사용자 데이터를 조용히 삭제하고 싶지 않음

그래서 해결책을 찾아보기 시작했습니다…

내가 찾은 것

  • 상태를 직렬화하지만 진화를 도와주지 않는 상태 라이브러리
  • 프레임워크에 밀접하게 결합돼 있거나 런타임 전용이며 느슨한 타입을 가진 마이그레이션 시스템

내가 원했던 것은 지루하고, 명시적이며, 안전한 것이었다.

그래서 직접 구현했다.
오늘은 이를 공유하여 다른 사람들이 직접 사용하거나 자신의 코드베이스에 유용한 아이디어를 차용할 수 있도록 한다.

아이디어: 영속된 상태를 스키마처럼 다루고, 블롭처럼 다루지 않기

도움을 준 사고 전환은 다음과 같습니다:

영속된 상태는 “그냥 JSON”이 아니라 버전이 있는 스키마를 가진 데이터입니다.

이를 받아들이면 나머지는 꽤 기계적으로 진행됩니다:

  1. 스키마 정의 (기본값 포함)
  2. 데이터와 함께 버전 저장
  3. 스키마가 변경될 때, 마이그레이션을 통해 기존 데이터를 최신 버전으로 옮김
  4. 모든 것을 검증

그게 전부입니다.

핵심 원칙 (내 비협상 항목)

  • Type safety end‑to‑end
  • Explicit migrations (no magic inference)
  • Deterministic upgrades (no “best effort”)
  • Storage‑agnostic (AsyncStorage, localStorage, memory)
  • Easy to delete later if I decide I don’t like it

그 마지막 요점은 사람들이 생각하는 것보다 더 중요합니다.

최소 예제

1️⃣ 스키마 정의 (Zod)

import { z } from "zod";

export const persistedSchema = z.object({
  _version: z.number(),
  preferences: z.object({
    colorScheme: z.enum(["system", "light", "dark"]).default("system"),
  }).default({ colorScheme: "system" }),
});

export type PersistedState = z.infer<typeof persistedSchema>;

Zod가 제공하는 것:

  • 런타임 검증
  • 컴파일‑타임 타입
  • 기본값 자동 제공

2️⃣ 스토리지 생성 (AsyncStorage, localStorage, 또는 메모리)

import { createPersistedState } from "@sebastianthiebaud/schema-versioned-storage";
import { createAsyncStorageAdapter } from
  "@sebastianthiebaud/schema-versioned-storage/adapters/async-storage";

const storage = createPersistedState({
  schema: persistedSchema,
  storageKey: "APP_STATE",
  storage: createAsyncStorageAdapter(),
  migrations: [],
  getCurrentVersion: () => 1,
});

await storage.init(); // 로드, 검증, 기본값 적용, 마이그레이션 실행

3️⃣ 사용하기 (완전 타입 지원)

// Read
const theme = storage.get("preferences").colorScheme;

// Write
await storage.set("preferences", {
  colorScheme: "dark",
});

키나 값을 잘못 입력하면, TypeScript가 런타임 이전에 경고합니다.

마이그레이션은 명시적이며 지루합니다 (설계상)

스키마가 변경될 때, 나는 마이그레이션을 추가합니다:

import type { Migration } from "@sebastianthiebaud/schema-versioned-storage";

const migration: Migration = {
  metadata: {
    version: 2,
    description: "Add language preference",
  },
  migrate: (state: unknown) => {
    // `as any` required here since the old schema is no more :-( 
    const old = state as any;
    return {
      ...old,
      _version: 2,
      preferences: {
        ...old.preferences,
        language: "en",
      },
    };
  },
};

export default migration;

추론 없음. 추측 없음. “아마도 동작할지도 모른다” 같은 말도 없습니다.
마이그레이션이 없거나 잘못된 경우, 초기화가 큰 소리로 실패합니다.

Adapters

Storage APIs differ just enough to be annoying, so I standardized on a tiny adapter interface:

interface StorageAdapter {
  getItem(key: string): Promise<string | null>;
  setItem(key: string, value: string): Promise<void>;
  removeItem(key: string): Promise<void>;
}

그 결과 나는 다음을 얻었다:

  • AsyncStorage (React Native용)
  • localStorage (웹용)
  • 테스트용 in‑memory 어댑터

in‑memory 어댑터를 사용해 마이그레이션을 테스트하는 것이 AsyncStorage를 모킹하는 것보다 훨씬 편리했다.

React 통합 (optional)

A thin context + hook avoids prop‑drilling:

const storage = useStorage();

No magic—just a thin wrapper around the same storage instance.

CLI – 마찰 제거

작은 CLI가 보일러플레이트를 생성합니다:

  • 마이그레이션 파일 생성
  • 마이그레이션 인덱스 생성
  • 변경 감지를 위한 스키마 해시

예시:

npx svs generate-migration --name add-field --from 1 --to 2

특별한 건 없고, 실수를 줄이는 것뿐입니다.

이게 “최고” 솔루션일까?

아마도 아닐 거예요.

하지만 다음과 같습니다:

  • 한 번에 이해하기 쉬움
  • 마음에 안 들면 쉽게 삭제 가능
  • 데이터가 어떻게 변하는지 명시적
  • 중요한 부분에서 타입 안전

이게 도움이 된다면 좋고 — 아니라면 아이디어를 자유롭게 가져가세요.

시작할 때 이 사고 모델에 맞는 것을 찾지 못해서 공유합니다. 즐거운 코딩 되세요!

직접 사용하거나, 일부를 복사하거나, 마이그레이션 패턴만 가져가도 제게는 성공입니다.

코드는 여기에서 확인할 수 있습니다:

👉 schema-versioned-storage on GitHub

React Native에서 지속된 상태 진화를 어떻게 다루는지 다른 사람들의 이야기를 듣고 싶어요 — 저도 아직 배우고 있습니다.

Back to Blog

관련 글

더 보기 »