브라우저 저장소는 서버 왕복 없이 사용자 상태를 유지하고 UX를 개선하는 데 유용합니다. React에서는 Local Storage와 Session Storage를 적절히 선택해 퍼시스턴스와 탭 범위를 관리합니다. 실무에서 바로 쓸 수 있는 패턴과 커스텀 훅을 소개합니다.
1. Local vs Session 핵심 차이
Local Storage는 브라우저를 닫아도 남는 영구 저장소이며, 동일 도메인의 모든 탭에서 공유됩니다. Session Storage는 탭마다 분리되고 탭을 닫으면 삭제됩니다. 용량은 브라우저별로 대략 5~10MB이며 문자열만 저장 가능합니다. 민감한 데이터는 저장하지 않습니다.
2. React에서 안전하게 읽기/쓰기
SSR 환경(Next.js 등)에서는 렌더 중 window가 없을 수 있습니다. 초기화 시 typeof window 체크로 안전하게 처리합니다. JSON.parse/JSON.stringify를 사용하고, 예외를 반드시 캐치합니다.
// 저장소 사용 가능 여부 체크(사파리 프라이빗 모드 대비)
export function isStorageAvailable(type = 'local') {
try {
const s = type === 'local' ? window.localStorage : window.sessionStorage;
const k = '__test__';
s.setItem(k, '1');
s.removeItem(k);
return true;
} catch (_) {
return false;
}
}
// TTL(만료) 지원 유틸
export function setWithTTL(storage, key, value, ttlMs) {
const payload = { value, expiresAt: Date.now() + ttlMs };
storage.setItem(key, JSON.stringify(payload));
}
export function getWithTTL(storage, key) {
const raw = storage.getItem(key);
if (!raw) return null;
try {
const obj = JSON.parse(raw);
if (!obj.expiresAt || Date.now() > obj.expiresAt) {
storage.removeItem(key);
return null;
}
return obj.value;
} catch (e) {
return null;
}
}3. 커스텀 훅: useStorage(로컬/세션 공통)
하나의 훅으로 로컬/세션을 모두 지원하면 컴포넌트가 단순해집니다. SSR 안전, JSON 직렬화, 탭 간 동기화를 포함합니다.
import * as React from 'react';
export function useStorage(key, initialValue, options = {}) {
const { type = 'local', listen = true } = options;
const isClient = typeof window !== 'undefined';
const storage = isClient ? (type === 'local' ? window.localStorage : window.sessionStorage) : null;
const getInitial = () => {
const fallback = typeof initialValue === 'function' ? initialValue() : initialValue;
if (!isClient) return fallback;
try {
const raw = storage.getItem(key);
return raw != null ? JSON.parse(raw) : fallback;
} catch (e) {
console.warn('Storage read failed', e);
return fallback;
}
};
const [state, setState] = React.useState(getInitial);
React.useEffect(() => {
if (!isClient) return;
try {
storage.setItem(key, JSON.stringify(state));
} catch (e) {
// 용량 초과 또는 권한 문제
console.warn('Storage write failed', e);
}
}, [key, state, isClient]);
React.useEffect(() => {
if (!isClient || !listen) return;
const handler = (e) => {
if (e.key === key && e.storageArea === storage) {
try {
setState(e.newValue != null ? JSON.parse(e.newValue) : (typeof initialValue === 'function' ? initialValue() : initialValue));
} catch (_) {}
}
};
window.addEventListener('storage', handler);
return () => window.removeEventListener('storage', handler);
}, [key, storage, listen, isClient]);
const remove = () => {
if (!isClient) return;
try { storage.removeItem(key); } catch (_) {}
setState(typeof initialValue === 'function' ? initialValue() : initialValue);
};
return [state, setState, remove];
}
// 별칭 훅
export const useLocalStorage = (key, initialValue, opts) => useStorage(key, initialValue, { type: 'local', ...opts });
export const useSessionStorage = (key, initialValue, opts) => useStorage(key, initialValue, { type: 'session', ...opts });4. 실전 예제: 테마, 폼 임시저장, 세션 상태
Local Storage는 사용자 선호도나 초안 저장에 적합합니다. Session Storage는 페이지 이동 중 임시 상태에 적합합니다.
// 테마 토글(로컬, 탭 간 공유)
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('app:theme', 'light');
React.useEffect(() => {
document.documentElement.dataset.theme = theme;
}, [theme]);
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
현재 테마: {theme}
</button>
);
}
// 폼 임시저장(로컬). 대용량이면 디바운싱 권장
function ProfileDraftForm() {
const [draft, setDraft, resetDraft] = useLocalStorage('app:profile:draft', { name: '', bio: '' });
const onChange = (e) => setDraft({ ...draft, [e.target.name]: e.target.value });
return (
<div>
<input name="name" value={draft.name} onChange={onChange} />
<textarea name="bio" value={draft.bio} onChange={onChange} />
<button onClick={() => resetDraft()}>초기화</button>
</div>
);
}
// 온보딩 진행 단계(세션, 탭별 분리)
function OnboardingSteps() {
const [step, setStep, reset] = useSessionStorage('app:onboarding:step', 1);
return (
<div>
<p>현재 단계: {step}</p>
<button onClick={() => setStep(step + 1)}>다음</button>
<button onClick={reset}>처음부터</button>
</div>
);
}5. 만료(Expiry)와 스키마 검증
배너 노출, 캐시 같은 데이터는 TTL을 두어 자동 만료시키면 UX가 좋아집니다. 또한 스키마를 검증해 파싱 오류나 과거 포맷을 대비합니다.
// 배너 노출 여부(24시간 유지)
function useDismissedBanner(id) {
const key = `app:banner:${id}`;
const isClient = typeof window !== 'undefined';
const [dismissed, setDismissed] = React.useState(false);
React.useEffect(() => {
if (!isClient) return;
const v = getWithTTL(window.localStorage, key);
setDismissed(Boolean(v));
}, [key, isClient]);
const dismiss = () => {
if (!isClient) return;
setWithTTL(window.localStorage, key, true, 24 * 60 * 60 * 1000);
setDismissed(true);
};
return [dismissed, dismiss];
}6. 탭 간 동기화와 이벤트
Local Storage는 같은 도메인의 다른 탭에서 변경되면 storage 이벤트가 발생합니다. 훅의 listen 옵션으로 실시간 동기화를 활성화하면 다중 탭 UX가 개선됩니다.
세션 저장소는 탭마다 분리되어 storage 이벤트를 활용한 동기화가 의미가 없습니다. 필요한 경우 BroadcastChannel이나 postMessage를 사용합니다.
7. 키 네이밍과 조직화
키에 네임스페이스를 포함하면 관리가 쉽습니다. 예: app:theme, app:cart, app:profile:draft처럼 기능별 접두사를 사용합니다. 데이터 구조 변경 시 마이그레이션을 위해 버전 접두사(v2:cart)도 고려합니다.
8. 에러 대응과 성능 팁
민감한 정보(토큰, 비밀번호)를 저장소에 두지 않습니다. 저장은 문자열만 가능하므로 JSON 직렬화를 사용합니다. 대용량 데이터는 저장 전 압축이나 서버 캐시를 고려합니다. QuotaExceededError 발생 시 캐치하고 유연한 폴백(UI 알림 또는 메모리 저장)으로 대응합니다.
입력에 따라 잦은 저장이 발생하면 디바운싱을 적용해 디스크 I/O를 줄입니다. SSR 환경에서는 초기 상태가 일치하지 않으면 하이드레이션 경고가 발생할 수 있으므로 useEffect에서 DOM 반영을 처리합니다.
// 간단 디바운스 저장 예시
function useDebouncedState(initial, delay = 300) {
const [state, setState] = React.useState(initial);
const [debounced, setDebounced] = React.useState(initial);
React.useEffect(() => {
const t = setTimeout(() => setDebounced(state), delay);
return () => clearTimeout(t);
}, [state, delay]);
return [debounced, setState];
}
function Notes() {
const [draft, setDraft] = useDebouncedState('', 400);
const [, setLocal] = useLocalStorage('app:notes', '');
React.useEffect(() => { setLocal(draft); }, [draft, setLocal]);
return <textarea value={draft} onChange={(e) => setDraft(e.target.value)} />;
}9. 언제 쓰지 말아야 하나
보안이 필요한 데이터, 서버와 강한 일관성이 필요한 상태, 매우 큰 객체는 저장소에 적합하지 않습니다. 이런 경우 서버 세션, 쿠키(필요 시 Secure/HttpOnly), IndexedDB 또는 캐싱 전략을 고려합니다.
10. 체크리스트 요약
Local/Session 선택 기준을 명확히 합니다. window 존재 여부를 체크합니다. try/catch로 안정성을 확보합니다. 키 네임스페이스와 버전을 관리합니다. TTL로 오래된 데이터를 청소합니다. storage 이벤트로 탭 동기화를 지원합니다. 민감 데이터는 저장하지 않습니다.
위 커스텀 훅과 유틸을 프로젝트에 추가하면 대부분의 저장소 요구사항을 간단하게 해결할 수 있습니다.
'React' 카테고리의 다른 글
| React에서 WebGL 직접 구현하여 인터랙티브 그래픽 만들기 (0) | 2026.05.20 |
|---|---|
| React에서 이미지 최적화를 위한 Responsive Image 구현하기 (0) | 2026.05.19 |
| React에서 JSON Schema 기반 폼 생성하기 (0) | 2026.05.18 |
| React 앱에서 Toast/Notification 시스템 구축하기 (0) | 2026.05.16 |
| React로 멀티 스텝(Form Wizard) 폼 구현하기 (0) | 2026.05.16 |