브라우저 저장소 비교 (LocalStorage vs SessionStorage vs Cookie vs IndexedDB)
중요도: ⭐⭐⭐⭐
웹 애플리케이션에서 클라이언트 측 데이터 저장을 위한 필수 지식입니다.
웹 애플리케이션에서 클라이언트 측 데이터 저장을 위한 다양한 옵션들의 특징과 사용 사례입니다.
저장소 비교표
| 특성 | LocalStorage | SessionStorage | Cookie | IndexedDB |
|---|---|---|---|---|
| 용량 | 5-10MB | 5-10MB | 4KB | 무제한* |
| 지속성 | 영구적 | 탭 닫으면 삭제 | 만료일 설정 가능 | 영구적 |
| 서버 전송 | 없음 | 없음 | 자동 전송 | 없음 |
| 스코프 | 오리진별 | 탭별 | 도메인별 | 오리진별 |
| API | 동기 | 동기 | 동기 | 비동기 |
| 데이터 타입 | 문자열만 | 문자열만 | 문자열만 | 모든 타입 |
1. LocalStorage
브라우저를 닫아도 데이터가 유지되는 영구적 저장소입니다.
기본 사용법
// 데이터 저장
localStorage.setItem(
"user",
JSON.stringify({
id: 1,
name: "John",
preferences: { theme: "dark", language: "ko" },
}),
);
// 데이터 읽기
const userData = JSON.parse(localStorage.getItem("user"));
console.log(userData); // { id: 1, name: 'John', ... }
// 데이터 삭제
localStorage.removeItem("user");
// 모든 데이터 삭제
localStorage.clear();
// 저장된 키 목록 조회
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
console.log(key, value);
}고급 활용법
// LocalStorage 헬퍼 클래스
class LocalStorageHelper {
constructor(prefix = "") {
this.prefix = prefix;
}
// 자동 직렬화/역직렬화
set(key, value, expiry = null) {
const item = {
value,
timestamp: Date.now(),
expiry,
};
localStorage.setItem(this.prefix + key, JSON.stringify(item));
}
get(key) {
try {
const itemStr = localStorage.getItem(this.prefix + key);
if (!itemStr) return null;
const item = JSON.parse(itemStr);
// 만료 시간 확인
if (item.expiry && Date.now() > item.expiry) {
this.remove(key);
return null;
}
return item.value;
} catch (error) {
console.error("LocalStorage get error:", error);
return null;
}
}
remove(key) {
localStorage.removeItem(this.prefix + key);
}
// 용량 확인
getSize() {
let total = 0;
for (let key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
total += localStorage[key].length + key.length;
}
}
return total;
}
// 만료된 항목들 정리
cleanup() {
const keys = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(this.prefix)) {
keys.push(key);
}
}
keys.forEach((key) => {
const shortKey = key.replace(this.prefix, "");
this.get(shortKey); // 만료 확인 및 자동 삭제
});
}
}
// 사용 예시
const storage = new LocalStorageHelper("myapp_");
// 1시간 후 만료되는 데이터 저장
storage.set("userToken", "abc123", Date.now() + 60 * 60 * 1000);
// 사용자 설정 저장
storage.set("settings", {
theme: "dark",
notifications: true,
autoSave: false,
});
// 장바구니 저장
storage.set("cart", [
{ id: 1, name: "상품1", quantity: 2 },
{ id: 2, name: "상품2", quantity: 1 },
]);주요 사용 사례
- 사용자 설정 (테마, 언어 등)
- 장바구니 내용
- 폼 데이터 임시 저장
- 오프라인 데이터 캐싱
2. SessionStorage
브라우저 탭이 열려있는 동안만 데이터가 유지되는 임시 저장소입니다.
기본 사용법
// 세션 동안만 유지되는 데이터
sessionStorage.setItem(
"currentSession",
JSON.stringify({
userId: 123,
startTime: Date.now(),
pagePath: window.location.pathname,
}),
);
// 데이터 읽기
const sessionData = JSON.parse(sessionStorage.getItem("currentSession"));
// 페이지 간 상태 전달
function navigateWithState(url, state) {
sessionStorage.setItem("navigationState", JSON.stringify(state));
window.location.href = url;
}
// 새 페이지에서 상태 복원
const navigationState = JSON.parse(sessionStorage.getItem("navigationState"));
if (navigationState) {
console.log("이전 페이지에서 전달된 상태:", navigationState);
// 사용 후 삭제
sessionStorage.removeItem("navigationState");
}다중 단계 폼 처리
// 다중 단계 폼 관리
class MultiStepForm {
constructor(formId) {
this.formId = formId;
this.storageKey = `form_${formId}`;
this.currentStep = 1;
this.loadSavedData();
}
// 현재 단계 데이터 저장
saveStep(stepNumber, data) {
const formData = this.getSavedData() || {};
formData[stepNumber] = {
...data,
timestamp: Date.now(),
};
sessionStorage.setItem(this.storageKey, JSON.stringify(formData));
}
// 저장된 데이터 불러오기
getSavedData() {
try {
const data = sessionStorage.getItem(this.storageKey);
return data ? JSON.parse(data) : null;
} catch (error) {
return null;
}
}
// 특정 단계 데이터 가져오기
getStepData(stepNumber) {
const formData = this.getSavedData();
return formData ? formData[stepNumber] : null;
}
// 폼 완성 후 데이터 정리
clearSavedData() {
sessionStorage.removeItem(this.storageKey);
}
// 이전에 저장된 데이터로 폼 복원
loadSavedData() {
const formData = this.getSavedData();
if (formData) {
// 각 단계별로 저장된 데이터 복원
Object.keys(formData).forEach((step) => {
this.populateStep(parseInt(step), formData[step]);
});
}
}
populateStep(stepNumber, data) {
// 해당 단계의 폼 필드에 데이터 채우기
Object.keys(data).forEach((fieldName) => {
const field = document.querySelector(
`[data-step="${stepNumber}"] [name="${fieldName}"]`,
);
if (field) {
field.value = data[fieldName];
}
});
}
}
// 사용 예시
const form = new MultiStepForm("registration");
// 1단계 완료 시
document.getElementById("step1-next").addEventListener("click", () => {
const stepData = {
firstName: document.getElementById("firstName").value,
lastName: document.getElementById("lastName").value,
email: document.getElementById("email").value,
};
form.saveStep(1, stepData);
// 다음 단계로 이동
});주요 사용 사례
- 다중 단계 폼 데이터
- 세션 기반 상태 관리
- 임시 데이터 저장
- 페이지 간 데이터 전달
3. Cookie
서버와 클라이언트 간 데이터 교환을 위한 전통적인 방법입니다.
Cookie 관리 유틸리티
// Cookie 헬퍼 클래스
class CookieManager {
// 쿠키 설정
static set(name, value, days = 7, options = {}) {
const expires = new Date();
expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);
let cookieString = `${name}=${encodeURIComponent(value)}; expires=${expires.toUTCString()}; path=${options.path || "/"}`;
if (options.domain) cookieString += `; domain=${options.domain}`;
if (options.secure) cookieString += "; secure";
if (options.httpOnly) cookieString += "; httpOnly";
if (options.sameSite) cookieString += `; sameSite=${options.sameSite}`;
document.cookie = cookieString;
}
// 쿠키 읽기
static get(name) {
const nameEQ = name + "=";
const ca = document.cookie.split(";");
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === " ") c = c.substring(1, c.length);
if (c.indexOf(nameEQ) === 0) {
return decodeURIComponent(c.substring(nameEQ.length, c.length));
}
}
return null;
}
// 쿠키 삭제
static remove(name, options = {}) {
this.set(name, "", -1, options);
}
// 모든 쿠키 가져오기
static getAll() {
const cookies = {};
document.cookie.split(";").forEach((cookie) => {
const parts = cookie.trim().split("=");
if (parts.length === 2) {
cookies[parts[0]] = decodeURIComponent(parts[1]);
}
});
return cookies;
}
// 쿠키 존재 여부 확인
static exists(name) {
return this.get(name) !== null;
}
}
// 사용 예시
// 기본 쿠키 설정
CookieManager.set("username", "john_doe");
// 보안 쿠키 설정 (HTTPS에서만 전송)
CookieManager.set("authToken", "abc123", 30, {
secure: true,
httpOnly: true, // JavaScript로 접근 불가
sameSite: "strict",
});
// 도메인별 쿠키
CookieManager.set(
"preferences",
JSON.stringify({
theme: "dark",
language: "ko",
}),
365,
{
domain: ".example.com", // 서브도메인에서도 접근 가능
secure: true,
sameSite: "lax",
},
);
// JWT 토큰 저장 (보안 고려)
CookieManager.set("jwt", token, 1, {
secure: true,
httpOnly: true,
sameSite: "lax",
});쿠키 동의 관리
// GDPR 쿠키 동의 관리
class CookieConsent {
constructor() {
this.consentTypes = ["necessary", "analytics", "marketing"];
this.init();
}
init() {
if (!this.hasConsent()) {
this.showConsentBanner();
} else {
this.loadConsentedCookies();
}
}
hasConsent() {
return CookieManager.exists("cookie_consent");
}
setConsent(consentData) {
CookieManager.set("cookie_consent", JSON.stringify(consentData), 365, {
necessary: true,
});
this.loadConsentedCookies();
}
getConsent() {
const consent = CookieManager.get("cookie_consent");
return consent ? JSON.parse(consent) : null;
}
canUse(type) {
const consent = this.getConsent();
return consent ? consent[type] === true : false;
}
loadConsentedCookies() {
const consent = this.getConsent();
if (consent.analytics) {
this.loadAnalytics();
}
if (consent.marketing) {
this.loadMarketing();
}
}
loadAnalytics() {
// Google Analytics 등 분석 도구 로드
if (this.canUse("analytics")) {
gtag("config", "GA_MEASUREMENT_ID");
}
}
loadMarketing() {
// 마케팅 도구 로드
if (this.canUse("marketing")) {
// Facebook Pixel, Google Ads 등
}
}
showConsentBanner() {
// 쿠키 동의 배너 표시
const banner = document.createElement("div");
banner.innerHTML = `
<div class="cookie-banner">
<p>이 사이트는 쿠키를 사용합니다.</p>
<button onclick="cookieConsent.setConsent({necessary: true, analytics: true, marketing: false})">
필수만 허용
</button>
<button onclick="cookieConsent.setConsent({necessary: true, analytics: true, marketing: true})">
모두 허용
</button>
</div>
`;
document.body.appendChild(banner);
}
}
// 전역 인스턴스
const cookieConsent = new CookieConsent();주요 사용 사례
- 인증 토큰
- 사용자 인증 상태
- 추적 및 분석
- 서버와 공유해야 하는 소량 데이터
4. IndexedDB
대용량 구조화된 데이터를 저장할 수 있는 브라우저 내장 NoSQL 데이터베이스입니다.
IndexedDB 래퍼 클래스
// IndexedDB 헬퍼 클래스
class IndexedDBHelper {
constructor(dbName, version = 1) {
this.dbName = dbName;
this.version = version;
this.db = null;
}
async init(schema) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this.db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
schema(db);
};
});
}
async add(storeName, data) {
const transaction = this.db.transaction([storeName], "readwrite");
const store = transaction.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.add(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async put(storeName, data) {
const transaction = this.db.transaction([storeName], "readwrite");
const store = transaction.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.put(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async get(storeName, key) {
const transaction = this.db.transaction([storeName], "readonly");
const store = transaction.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async getAll(storeName, query = null, count = null) {
const transaction = this.db.transaction([storeName], "readonly");
const store = transaction.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.getAll(query, count);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async delete(storeName, key) {
const transaction = this.db.transaction([storeName], "readwrite");
const store = transaction.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.delete(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 인덱스를 통한 검색
async getByIndex(storeName, indexName, query) {
const transaction = this.db.transaction([storeName], "readonly");
const store = transaction.objectStore(storeName);
const index = store.index(indexName);
return new Promise((resolve, reject) => {
const request = index.getAll(query);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// 커서를 사용한 순차 접근
async iterate(storeName, callback) {
const transaction = this.db.transaction([storeName], "readonly");
const store = transaction.objectStore(storeName);
return new Promise((resolve, reject) => {
const request = store.openCursor();
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
callback(cursor.value, cursor.key);
cursor.continue();
} else {
resolve();
}
};
request.onerror = () => reject(request.error);
});
}
}
// 사용 예시
const dbHelper = new IndexedDBHelper("MyAppDB", 1);
// 데이터베이스 스키마 정의
await dbHelper.init((db) => {
// Users 스토어 생성
if (!db.objectStoreNames.contains("users")) {
const userStore = db.createObjectStore("users", {
keyPath: "id",
autoIncrement: true,
});
userStore.createIndex("email", "email", { unique: true });
userStore.createIndex("name", "name", { unique: false });
}
// Posts 스토어 생성
if (!db.objectStoreNames.contains("posts")) {
const postStore = db.createObjectStore("posts", {
keyPath: "id",
autoIncrement: true,
});
postStore.createIndex("userId", "userId", { unique: false });
postStore.createIndex("createdAt", "createdAt", { unique: false });
}
// Files 스토어 (바이너리 데이터 저장)
if (!db.objectStoreNames.contains("files")) {
const fileStore = db.createObjectStore("files", {
keyPath: "id",
autoIncrement: true,
});
fileStore.createIndex("filename", "filename", { unique: false });
fileStore.createIndex("mimetype", "mimetype", { unique: false });
}
});오프라인 앱을 위한 데이터 관리
// 오프라인 데이터 매니저
class OfflineDataManager {
constructor() {
this.dbHelper = new IndexedDBHelper("OfflineApp", 1);
this.syncQueue = [];
}
async init() {
await this.dbHelper.init((db) => {
// 캐시된 API 응답
const apiCache = db.createObjectStore("apiCache", { keyPath: "url" });
apiCache.createIndex("timestamp", "timestamp");
// 동기화 대기열
const syncQueue = db.createObjectStore("syncQueue", {
keyPath: "id",
autoIncrement: true,
});
syncQueue.createIndex("action", "action");
syncQueue.createIndex("timestamp", "timestamp");
});
// 온라인 상태 모니터링
window.addEventListener("online", () => this.syncWhenOnline());
window.addEventListener("offline", () => console.log("오프라인 모드"));
}
// API 응답 캐시
async cacheApiResponse(url, data) {
await this.dbHelper.put("apiCache", {
url,
data,
timestamp: Date.now(),
});
}
// 캐시된 API 응답 가져오기
async getCachedResponse(url, maxAge = 300000) {
// 5분
const cached = await this.dbHelper.get("apiCache", url);
if (cached && Date.now() - cached.timestamp < maxAge) {
return cached.data;
}
return null;
}
// 오프라인 시 동작 큐에 추가
async addToSyncQueue(action, data) {
await this.dbHelper.add("syncQueue", {
action,
data,
timestamp: Date.now(),
synced: false,
});
}
// 온라인 시 동기화
async syncWhenOnline() {
if (!navigator.onLine) return;
const pendingActions = await this.dbHelper.getByIndex(
"syncQueue",
"synced",
false,
);
for (const action of pendingActions) {
try {
await this.performSync(action);
// 동기화 성공 시 큐에서 제거
await this.dbHelper.delete("syncQueue", action.id);
} catch (error) {
console.error("동기화 실패:", error);
}
}
}
async performSync(action) {
switch (action.action) {
case "create":
await fetch("/api/items", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(action.data),
});
break;
case "update":
await fetch(`/api/items/${action.data.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(action.data),
});
break;
case "delete":
await fetch(`/api/items/${action.data.id}`, {
method: "DELETE",
});
break;
}
}
}
// 파일 저장 및 관리
class FileManager {
constructor(dbHelper) {
this.dbHelper = dbHelper;
}
// 파일 저장 (바이너리 데이터)
async saveFile(file, metadata = {}) {
const fileData = {
filename: file.name,
mimetype: file.type,
size: file.size,
data: await file.arrayBuffer(), // 바이너리 데이터
uploadedAt: Date.now(),
...metadata,
};
return await this.dbHelper.add("files", fileData);
}
// 파일 읽기
async getFile(id) {
return await this.dbHelper.get("files", id);
}
// 파일을 Blob으로 변환
async getFileAsBlob(id) {
const fileData = await this.getFile(id);
if (!fileData) return null;
return new Blob([fileData.data], { type: fileData.mimetype });
}
// 파일을 URL로 변환 (이미지 표시 등을 위해)
async getFileURL(id) {
const blob = await this.getFileAsBlob(id);
return blob ? URL.createObjectURL(blob) : null;
}
}
// 사용 예시
const offlineManager = new OfflineDataManager();
await offlineManager.init();
const fileManager = new FileManager(offlineManager.dbHelper);
// 파일 업로드 처리
document.getElementById("fileInput").addEventListener("change", async (e) => {
const file = e.target.files[0];
if (file) {
const fileId = await fileManager.saveFile(file, {
category: "profile_image",
userId: getCurrentUserId(),
});
console.log("파일 저장됨:", fileId);
}
});주요 사용 사례
- 대용량 데이터 저장
- 오프라인 애플리케이션
- 이미지, 파일 등 바이너리 데이터
- 복잡한 구조의 데이터
- PWA 데이터 저장
저장소 선택 가이드
언제 어떤 것을 사용할까?
- LocalStorage: 간단한 설정, 소량 데이터, 영구 저장
- SessionStorage: 임시 데이터, 탭 범위 데이터
- Cookie: 서버와 공유 필요, 인증 관련, 소량 데이터
- IndexedDB: 대용량 데이터, 복잡한 쿼리, 오프라인 지원
보안 고려사항
보안 주의사항
- 민감한 정보는 암호화 후 저장
- XSS 공격에 주의 (모든 클라이언트 저장소는 JavaScript로 접근 가능)
- Cookie의 경우 httpOnly, secure, sameSite 속성 활용
- 저장된 데이터는 사용자가 언제든 삭제할 수 있음을 고려
// 데이터 암호화 예시 (crypto-js 라이브러리 사용)
import CryptoJS from "crypto-js";
class SecureStorage {
constructor(secretKey) {
this.secretKey = secretKey;
}
encrypt(data) {
return CryptoJS.AES.encrypt(
JSON.stringify(data),
this.secretKey,
).toString();
}
decrypt(encryptedData) {
try {
const bytes = CryptoJS.AES.decrypt(encryptedData, this.secretKey);
return JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
} catch (error) {
return null;
}
}
setSecure(key, data) {
const encrypted = this.encrypt(data);
localStorage.setItem(key, encrypted);
}
getSecure(key) {
const encrypted = localStorage.getItem(key);
return encrypted ? this.decrypt(encrypted) : null;
}
}
// 사용
const secureStorage = new SecureStorage("user-secret-key");
secureStorage.setSecure("sensitiveData", {
token: "abc123",
userId: 456,
});면접 팁
각 저장소의 특징을 단순히 암기하기보다는 실제 프로젝트에서 어떤 상황에서 어떤 저장소를 선택했는지, 그리고 그 이유를 구체적인 예시와 함께 설명할 수 있어야 합니다.
Edit on GitHub
Last updated on