Skip to content

useStorageState

localStorage 또는 sessionStorage와 컴포넌트 상태를 동기화하는 Hook입니다. React(CSR), Next.js Pages Router, App Router에서 모두 안전하게 동작합니다.

useLocalStorage, useSessionStorage는 각각 storage: 'local', storage: 'session'을 적용한 얇은 래퍼입니다.

무엇을 쓸까: 일상적인 경우엔 의도가 또렷한 useLocalStorage / useSessionStorage를 쓰세요. useStorageState는 storage 종류를 동적으로 받거나 코어 API가 필요할 때 사용합니다.

Return

readonly [value, setValue, removeValue] 튜플을 반환합니다.

  • value - 현재 저장된 값입니다. defaultValue를 주면 그 타입으로, 주지 않으면 T | undefined로 좁혀집니다.
  • setValue - Dispatch<SetStateAction<T>>. 값 또는 함수형 업데이터를 받습니다. 영속에 성공한 경우에만 값이 갱신됩니다(아래 "에러 처리" 참고).
  • removeValue - 키 삭제에 성공하면 값을 defaultValue로 되돌립니다.

Options

옵션타입기본값설명
storage'local' | 'session''local'사용할 Web Storage 종류
defaultValueTundefined키가 없거나 파싱 실패 시 사용하는 폴백
serializer(value: T) => stringJSON.stringify값 → 문자열 직렬화
deserializer(raw: string) => TJSON.parse문자열 → 값 역직렬화
syncbooleantrue다른 탭의 변경을 storage 이벤트로 동기화
onError(error: unknown, operation: 'write' | 'remove') => voidundefined쓰기/삭제 실패 시 호출(파싱 실패는 제외)

Usage

tsx
import { useStorageState, useLocalStorage } from '@teamsparta/react';

function Example() {
  const [theme, setTheme, removeTheme] = useLocalStorage('theme', {
    defaultValue: 'light',
  });

  return (
    <div>
      <p>현재 테마: {theme}</p>
      <button onClick={() => setTheme('dark')}>다크</button>
      <button onClick={() => setTheme((prev) => (prev === 'dark' ? 'light' : 'dark'))}>
        토글
      </button>
      <button onClick={removeTheme}>초기화</button>
    </div>
  );
}

Example

sessionStorage 사용

tsx
import { useSessionStorage } from '@teamsparta/react';

function FunnelStep() {
  const [step, setStep] = useSessionStorage('funnel-step', { defaultValue: 0 });

  return <button onClick={() => setStep((prev) => (prev ?? 0) + 1)}>다음</button>;
}

객체 저장

tsx
type Preferences = {
  fontSize: number;
  language: string;
};

function Settings() {
  const [preferences, setPreferences] = useLocalStorage<Preferences>('preferences', {
    defaultValue: { fontSize: 14, language: 'ko' },
  });

  return (
    <button onClick={() => setPreferences((prev) => ({ ...prev!, fontSize: 16 }))}>
      글자 크게
    </button>
  );
}

커스텀 직렬화 (Date)

tsx
const [lastVisited, setLastVisited] = useLocalStorage<Date>('last-visited', {
  defaultValue: new Date(),
  serializer: (value) => value.toISOString(),
  deserializer: (raw) => new Date(raw),
});

다른 탭 동기화 끄기

tsx
// 같은 탭 인스턴스 동기화는 유지되지만, 다른 탭의 변경은 반영하지 않습니다.
const [draft, setDraft] = useLocalStorage('draft', { defaultValue: '', sync: false });

영속 실패 감지 (onError)

tsx
const [cart, setCart] = useLocalStorage('cart', {
  defaultValue: [],
  // 용량 초과·시크릿 모드 등으로 저장 실패 시 호출됩니다.
  onError: (error, operation) => {
    toast.error('장바구니를 저장하지 못했습니다.');
    captureException(error, { tags: { operation } });
  },
});

주의사항

  • 에러 처리(storage-first): 용량 초과(QuotaExceededError)·시크릿 모드(SecurityError)·스토리지 비활성 등으로 쓰기/삭제가 실패하면 값을 변경하지 않고 onError로 알립니다. 즉 화면에 보이는 값은 항상 실제로 저장된 값과 일치하며, "저장된 줄 알았는데 새로고침하면 사라지는" 상황이 생기지 않습니다. (개발 모드에서는 console.warn도 출력됩니다.) 영속 실패에도 현재 탭 UI 동작을 유지해야 한다면 onError로 직접 상태를 관리하세요.
  • 하이드레이션: 서버와 하이드레이션 시점에는 항상 defaultValue를 반환하고, 마운트 이후 실제 스토리지 값으로 동기화합니다. 이 과정에서 Next.js 등 하이드레이션 환경에서는 마운트 직후 1회 추가 렌더가 발생할 수 있습니다(useSyncExternalStore의 동작이며 회피할 수 없습니다).
  • App Router: 브라우저 API를 사용하므로 클라이언트 컴포넌트('use client')에서만 사용하세요.
  • 같은 탭 동기화: 같은 키를 사용하는 같은 탭 내 여러 인스턴스는 항상 동기화됩니다. 단, 같은 키에는 **동일한 타입·serializer/deserializer·defaultValue**를 사용하세요(공유 스냅샷을 함께 쓰므로).
  • 다른 탭 동기화: 브라우저의 storage 이벤트는 변경을 발생시킨 탭에서는 발화하지 않으므로, 같은 탭 동기화와 다른 탭 동기화를 별도로 처리합니다. 한편 sessionStorage는 탭 단위로 격리되어 일반적인 탭 간 동기화 대상이 아닙니다.
  • defaultValue: useState의 초기값처럼 최초 렌더의 값만 사용됩니다(키 부재/파싱 실패 시 폴백). 리렌더 중 defaultValue를 바꿔도 반영되지 않습니다.