브라우저에서 데이터를 저장할 때 가장 많이 사용하는 두 가지 방법이 localStorage와 sessionStorage입니다. 이름이 비슷해서 헷갈리기 쉽지만, 용도와 생명주기가 명확히 다릅니다. 이번 글에서 두 가지의 차이와 올바른 사용법을 정리해봅니다.
웹 스토리지 API
두 스토리지 모두 Web Storage API의 일부입니다. 쿠키와 달리 서버로 자동 전송되지 않고, 순수하게 브라우저 측 저장소로만 동작합니다. 공통 메서드도 동일합니다.
// 저장
storage.setItem('key', 'value');
// 읽기
const val = storage.getItem('key');
// 삭제
storage.removeItem('key');
// 전체 초기화
storage.clear();
객체나 배열은 문자열로만 저장되므로 JSON.stringify / JSON.parse를 사용해야 합니다.
// 객체 저장
localStorage.setItem('user', JSON.stringify({ name: '홍길동', age: 30 }));
// 객체 읽기
const user = JSON.parse(localStorage.getItem('user'));
console.log(user.name); // '홍길동'
핵심 차이: 생명주기
| 항목 | localStorage | sessionStorage |
|---|---|---|
| 데이터 유지 | 브라우저를 닫아도 유지 | 탭/창을 닫으면 삭제 |
| 범위 | 같은 출처(origin) 전체 | 같은 탭(세션)으로 한정 |
| 용량 | 약 5~10MB | 약 5MB |
| 공유 | 같은 출처 탭 간 공유 | 탭 간 공유 안 됨 |
localStorage — 영구 저장
사용자가 명시적으로 삭제하거나 코드로 clear()를 호출하지 않는 한 데이터가 영구히 유지됩니다. 다크모드 설정처럼 새로고침 후에도 유지되어야 하는 사용자 설정에 적합합니다.
// 다크모드 설정 저장 예시
const toggle = document.getElementById('themeToggle');
toggle.addEventListener('click', () => {
const current = document.documentElement.getAttribute('data-theme');
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
localStorage.setItem('theme', next); // 영구 저장
});
// 페이지 로드 시 복원
const saved = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', saved);
localStorage는 같은 출처(도메인 + 프로토콜 + 포트)의 모든 탭에서 공유됩니다. 탭 A에서 값을 바꾸면 탭 B에서도 즉시 반영됩니다 — storage 이벤트로 감지 가능합니다.
// 다른 탭의 스토리지 변경 감지
window.addEventListener('storage', (e) => {
if (e.key === 'theme') {
document.documentElement.setAttribute('data-theme', e.newValue);
}
});
sessionStorage — 임시 저장
탭 또는 창을 닫으면 데이터가 자동으로 삭제됩니다. 같은 탭에서만 유효하므로 로그인 세션이나 멀티 스텝 폼의 임시 데이터에 적합합니다.
// 멀티 스텝 폼 임시 저장
function saveStep(step, data) {
sessionStorage.setItem(`form_step_${step}`, JSON.stringify(data));
}
function loadStep(step) {
return JSON.parse(sessionStorage.getItem(`form_step_${step}`));
}
// 1단계 저장
saveStep(1, { name: '홍길동', email: 'hong@example.com' });
// 2단계에서 불러오기
const step1 = loadStep(1);
console.log(step1.name); // '홍길동'
언제 무엇을 쓸까 — 결정 가이드
- ✅ localStorage — 다크모드, 언어 설정, 최근 검색어, 장바구니(비로그인)
- ✅ sessionStorage — 멀티 스텝 폼, 임시 필터 상태, 스크롤 위치
- ⚠️ 둘 다 — 민감한 정보(비밀번호, 토큰)는 저장하지 마세요. XSS 공격에 취약합니다. 인증 토큰은 HttpOnly 쿠키를 사용하세요.
영구적으로 남겨야 하면
localStorage, 탭을 닫으면 사라져야 하면 sessionStorage. 민감한 데이터는 절대 저장하지 말 것.
쿠키(Cookie)와 비교
| 항목 | localStorage | sessionStorage | Cookie |
|---|---|---|---|
| 용량 | 5~10MB | 5MB | 4KB |
| 서버 자동 전송 | 안 됨 | 안 됨 | 자동 전송 |
| 만료 설정 | 직접 관리 | 탭 닫으면 삭제 | 만료일 설정 가능 |
| JavaScript 접근 | 가능 | 가능 | HttpOnly 설정 시 불가 |
| XSS 취약성 | 높음 | 높음 | HttpOnly로 완화 |
인증 토큰은 반드시 HttpOnly + Secure + SameSite=Strict 쿠키로 저장하세요. localStorage에 저장한 토큰은 XSS 공격 하나로 탈취됩니다.
IndexedDB — 대용량 데이터 저장
5MB를 초과하는 데이터나 구조화된 객체를 저장할 때는 IndexedDB를 사용합니다. localStorage보다 복잡하지만, Dexie.js 같은 라이브러리로 편리하게 사용할 수 있습니다.
// Dexie.js 사용 예시 (npm install dexie)
import Dexie from 'dexie';
const db = new Dexie('MyDatabase');
db.version(1).stores({
posts: '++id, title, createdAt',
users: '++id, name, email'
});
// 데이터 추가
await db.posts.add({
title: '오프라인 저장 예시',
content: '...',
createdAt: new Date()
});
// 최근 게시글 10개 조회
const recentPosts = await db.posts
.orderBy('createdAt')
.reverse()
.limit(10)
.toArray();
IndexedDB는 오프라인 웹앱, 대용량 캐시, 동기화 큐 등에 적합합니다. PWA(Progressive Web App)에서 오프라인 데이터 저장의 핵심 기술로 활용됩니다.
스토리지 용량 초과 예외 처리
localStorage 용량(보통 5MB)을 초과하면 QuotaExceededError가 발생합니다. 이를 대비한 방어 코드를 항상 작성하세요.
function safeSetItem(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (e) {
if (e.name === 'QuotaExceededError') {
// 오래된 캐시 정리 후 재시도
clearOldCache();
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch {
console.warn('스토리지 용량 초과, 저장 실패:', key);
return false;
}
}
throw e;
}
}
// 현재 사용 중인 스토리지 용량 확인
function getStorageUsage() {
let total = 0;
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
total += (localStorage.getItem(key).length + key.length) * 2;
}
return `${(total / 1024).toFixed(2)} KB`;
}
브라우저 저장소 선택 가이드
상황에 맞는 저장소를 선택하는 기준을 정리하면 다음과 같습니다.
- localStorage — 다크모드, 언어 설정, 최근 검색어 등 영구 사용자 설정
- sessionStorage — 멀티 스텝 폼 임시 데이터, 스크롤 위치, 세션 내 필터 상태
- HttpOnly Cookie — 인증 토큰, 세션 ID. XSS에 안전
- IndexedDB — 오프라인 데이터, 대용량 캐시, 구조화된 데이터
- Cache API — 네트워크 응답(API 결과, 이미지, 스크립트) 캐싱. Service Worker와 함께 사용
각 저장소의 목적을 명확히 구분해서 사용해야 보안 취약점을 만들지 않고, 앱의 성능과 사용자 경험을 동시에 향상시킬 수 있습니다.
localStorage 보안 주의사항과 실무 패턴
localStorage는 편리하지만 보안 측면에서 주의가 필요합니다. 가장 중요한 원칙은 인증 토큰이나 비밀번호, 개인 식별 정보를 localStorage에 저장하지 않는 것입니다. XSS(크로스 사이트 스크립팅) 취약점이 하나라도 있으면 공격자가 JavaScript로 localStorage 전체를 읽어갈 수 있기 때문입니다. 인증 토큰은 반드시 HttpOnly 쿠키로 관리해야 JavaScript에서 접근 자체가 불가능합니다.
사용자 설정이나 UI 상태처럼 유출되어도 큰 피해가 없는 데이터만 localStorage에 저장하는 것이 안전합니다. 저장할 때는 직렬화 과정에서 발생할 수 있는 오류에 대비해 항상 try-catch로 감싸야 합니다. JSON.parse()가 실패하면 앱 전체가 죽을 수 있기 때문입니다. 또한 사용자가 프라이빗 브라우징 모드에서 사용하거나, 브라우저 설정에서 서드파티 쿠키와 스토리지를 차단한 경우 localStorage 접근 자체가 예외를 던질 수 있습니다. 이런 환경을 감지하고 메모리 기반 폴백을 제공하는 안전한 스토리지 유틸리티를 만들어 두면 프로덕션 안정성이 크게 높아집니다.
스토리지 이벤트를 활용한 탭 간 상태 동기화
localStorage의 강점 중 하나는 같은 도메인의 다른 탭에서 변경이 발생할 때 storage 이벤트를 받을 수 있다는 점입니다. 이 특성을 활용하면 여러 탭을 동시에 열어둔 사용자에게 일관된 경험을 제공할 수 있습니다. 예를 들어 한 탭에서 로그아웃하면 다른 탭도 자동으로 로그아웃 상태로 전환되도록 구현할 수 있습니다. 쇼핑몰 장바구니나 알림 설정 같은 공유 상태도 탭 간 동기화 대상으로 적합합니다. 다만 이벤트는 변경이 발생한 탭 자신에게는 발생하지 않고, 다른 탭에만 전달된다는 점을 기억해야 합니다. BroadcastChannel API와 조합하면 같은 탭 내에서도 메시지를 전달할 수 있어 더 유연한 탭 간 통신이 가능합니다.