Afaik

브라우저 저장소 비교 (LocalStorage vs SessionStorage vs Cookie vs IndexedDB)

중요도: ⭐⭐⭐⭐

웹 애플리케이션에서 클라이언트 측 데이터 저장을 위한 필수 지식입니다.

웹 애플리케이션에서 클라이언트 측 데이터 저장을 위한 다양한 옵션들의 특징과 사용 사례입니다.

저장소 비교표

특성LocalStorageSessionStorageCookieIndexedDB
용량5-10MB5-10MB4KB무제한*
지속성영구적탭 닫으면 삭제만료일 설정 가능영구적
서버 전송없음없음자동 전송없음
스코프오리진별탭별도메인별오리진별
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);
  // 다음 단계로 이동
});

주요 사용 사례

  • 다중 단계 폼 데이터
  • 세션 기반 상태 관리
  • 임시 데이터 저장
  • 페이지 간 데이터 전달

서버와 클라이언트 간 데이터 교환을 위한 전통적인 방법입니다.

// 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 데이터 저장

저장소 선택 가이드

언제 어떤 것을 사용할까?

  1. LocalStorage: 간단한 설정, 소량 데이터, 영구 저장
  2. SessionStorage: 임시 데이터, 탭 범위 데이터
  3. Cookie: 서버와 공유 필요, 인증 관련, 소량 데이터
  4. 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