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 종류 |
defaultValue | T | undefined | 키가 없거나 파싱 실패 시 사용하는 폴백 |
serializer | (value: T) => string | JSON.stringify | 값 → 문자열 직렬화 |
deserializer | (raw: string) => T | JSON.parse | 문자열 → 값 역직렬화 |
sync | boolean | true | 다른 탭의 변경을 storage 이벤트로 동기화 |
onError | (error: unknown, operation: 'write' | 'remove') => void | undefined | 쓰기/삭제 실패 시 호출(파싱 실패는 제외) |
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를 바꿔도 반영되지 않습니다.
