Frontend··6 min read·

브라우저 저장소 비교 — localStorage, sessionStorage, Cookie, IndexedDB

클라이언트에 데이터를 저장하는 네 가지 방법의 용량·수명·서버 전송·동기 여부를 비교하고, 무엇을 언제 써야 하는지와 민감 정보 저장의 위험을 정리했다.

브라우저 저장소 비교

JavaScript는 어디서 실행되는가에서 localStorage, IndexedDB 같은 것들이 엔진이 아니라 브라우저가 제공하는 Web API라고 정리했다. 이 글은 그 저장소들을 실제로 비교한다. 클라이언트에 데이터를 두는 방법은 하나가 아니고, 용량·수명·서버 전송 여부·동기 여부가 다 달라서 용도에 맞게 골라야 한다.


한눈에 비교

localStoragesessionStorageCookieIndexedDB
용량약 5~10MB약 5MB약 4KB대용량(수백 MB+)
수명영구(지울 때까지)탭 닫으면 삭제만료일까지영구
서버 전송안 됨안 됨매 요청 자동 첨부안 됨
접근 방식동기동기동기(문자열)비동기
저장 형태문자열만문자열만문자열만객체·Blob 등 구조화
범위originorigin + 탭도메인/경로origin

수명 — 무엇이 언제 사라지나

가장 자주 헷갈리는 축이 수명이다. "한 번 저장하면 계속 남는가"를 기준으로 보면 이렇게 갈린다.

상황localStoragesessionStorageCookieIndexedDB
탭 닫기유지삭제유지(영구 쿠키)유지
브라우저 재시작유지삭제됨만료 전이면 유지유지
다른 탭에서 접근가능(공유)불가(탭별 격리)가능가능
다른 브라우저·기기불가(각자 따로)불가불가불가
시크릿 창 종료삭제삭제삭제삭제

핵심은 두 가지다.

  • 계속 남는 건 `localStorage`와 `IndexedDB` 둘 뿐이다. sessionStorage는 탭을 닫으면 사라지고, 쿠키는 만료일에 달렸다.
  • 어느 저장소든 브라우저·기기마다 별개다. 한 곳에 저장했다고 다른 브라우저에서 보이지 않는다. (서버에 저장하지 않는 한)

그래서 "한 번 설정하면 계속 유지되는 클라이언트 플래그"가 필요하면 localStorage가 정답이다 — 단, 기기/브라우저마다 최초 1회는 설정해야 한다.


localStorage / sessionStorage (Web Storage)

가장 간단한 키-값 저장소다. API가 거의 동일하고 수명만 다르다.

js
localStorage.setItem("theme", "dark");
localStorage.getItem("theme");   // "dark"
localStorage.removeItem("theme");

특징과 주의점은 이렇다.

  • 문자열만 저장된다. 객체는 JSON.stringify로 직렬화해서 넣고 꺼낼 때 JSON.parse 한다.
  • 동기 API다. 큰 데이터를 자주 읽고 쓰면 메인 스레드를 막을 수 있다.
  • localStorage는 명시적으로 지울 때까지 남고, sessionStorage그 탭을 닫으면 사라진다. (탭마다 별개)
  • SSR 환경에서는 서버에 localStorage가 없으므로, 렌더 중이 아니라 useEffect나 이벤트 핸들러에서 접근해야 한다. (관련: js-engine-vs-runtime의 CSR vs SSR 절)

용도: 테마, 최근 본 항목, 접힘 상태 같은 가벼운 UI 상태.

실전 예: 이 블로그의 글 조회수에서 운영자 본인 조회를 제외할 때 localStorage를 썼다. localStorage.setItem("blog_no_count", "1") 플래그가 있으면 조회 기록 요청을 생략한다. sessionStorage였다면 탭을 닫을 때마다 다시 설정해야 하고, 쿠키였다면 굳이 매 요청에 실려 서버로 갈 이유가 없다. "브라우저에 한 번 켜두고 계속 유지되는 클라이언트 전용 플래그"라는 요구에 localStorage가 가장 맞았다. (단, 기기·브라우저마다 1회 설정 필요)

쿠키는 다른 저장소와 결정적으로 다른 점이 있다. 같은 도메인 요청마다 자동으로 서버에 함께 전송된다.

text
서버 → 브라우저:  Set-Cookie: session=xyz; HttpOnly; Secure; SameSite=Lax
브라우저 → 서버:  (이후 모든 요청에) Cookie: session=xyz

그래서 쿠키는 "저장"보다 "서버와의 상태 공유" 도구에 가깝다. 주요 속성은 보안과 직결된다.

  • HttpOnly: JS(document.cookie)에서 접근 불가. XSS로 토큰을 훔치기 어렵게 만든다.
  • Secure: HTTPS에서만 전송.
  • SameSite: 다른 사이트발 요청에 쿠키를 붙일지 제어. CSRF 방어.

용도: 인증 세션/토큰. 특히 HttpOnly 쿠키는 JS로 읽을 수 없어 인증 정보를 두기에 가장 안전한 축이다. 단, 용량이 4KB로 작고 매 요청에 실리므로 큰 데이터엔 부적합하다. (HTTP 기초의 무상태·쿠키 절과 이어진다.)


IndexedDB

브라우저에 내장된 비동기 트랜잭션 데이터베이스다. 위 셋과 체급이 다르다.

  • 대용량: 수백 MB 이상도 가능하다.
  • 비동기: 메인 스레드를 막지 않는다.
  • 구조화된 데이터: 문자열뿐 아니라 객체, 배열, Blob, ArrayBuffer까지 그대로 저장한다.
  • 인덱스와 트랜잭션을 지원한다.

원시 API는 이벤트 기반이라 다소 번거로워서, 실무에서는 idb 같은 얇은 래퍼를 많이 쓴다.

용도: 오프라인 데이터, 캐시, 큰 바이너리(이미지·파일), 대량 목록. PWA의 오프라인 저장소가 대표적이다.


실무 — IndexedDB를 유틸 함수로 감싸기

원시 IndexedDB API는 두 가지가 불편하다. 이벤트 기반이라 request.onsuccess/onerror 콜백을 매번 달아야 하고, 트랜잭션과 object store를 호출할 때마다 새로 열어야 한다. SQL 에디터를 만들면서 탭과 작성 중인 쿼리를 IndexedDB에 저장했는데, 컴포넌트에서 이 보일러플레이트를 직접 쓰지 않도록 한 겹 감싸 addTab(...), getAllContents(...) 같은 도메인 함수만 노출했다.

1. 이벤트 API를 Promise로 래핑

모든 작동(add·get·edit·del·clear)을 한 함수로 모으고, 콜백을 Promise로 바꾼다.

js
function makePromise(runType, dbName, storageName, value, onSuccess) {
  return new Promise((resolve, reject) => {
    const request = createIdbRequest(dbName); // open + onupgradeneeded

    request.onsuccess = () => {
      const db = request.result;
      const tx = db.transaction(storageName, 'readwrite');
      const store = tx.objectStore(storageName);

      let op;
      switch (runType) {
        case 'add':   op = store.add(value); break;
        case 'get':   op = value === undefined ? store.getAll() : store.get(value); break;
        case 'edit':  op = store.put(value); break;
        case 'del':   op = store.delete(value); break;
        case 'clear': op = store.clear(); break;
        default: console.error('unknown runType', runType); return;
      }

      op.onsuccess = () => onSuccess(op, resolve, reject);
      op.onerror = (e) => console.error('IDB op error', e);
    };
  });
}

runType 하나로 CRUD를 분기하니 트랜잭션을 여는 코드가 한 곳에만 남는다.

2. object store는 onupgradeneeded에서 한 번만 생성

스키마(저장소) 생성은 DB 버전이 올라갈 때 호출되는 onupgradeneeded에서만 한다.

js
function makeObjectStore(db, name) {
  if (!db.objectStoreNames.contains(name)) {
    db.createObjectStore(name, { keyPath: 'id' });
  }
}

request.onupgradeneeded = () => {
  const db = request.result;
  makeObjectStore(db, TAB_STORAGE);
  makeObjectStore(db, CONTENTS_STORAGE);
};

스키마를 바꾸려면 버전(idbVersion)을 올려야 다시 호출된다. SSR 환경을 위해 window 가드와 벤더 프리픽스 폴백도 둔다.

js
const idb = typeof window !== 'undefined'
  ? window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB
  : null;

3. 도메인 함수로 노출

제네릭 한 겹 위에 저장소별 얇은 래퍼를 얹는다. 컴포넌트는 IndexedDB를 전혀 모르고 addTab/getAllContents만 부른다.

js
const addData = (db, store, v, cb) =>
  makePromise('add', db, store, v, resolveResult).then(cb);

export const addTab      = (db, v, cb) => addData(db, TAB_STORAGE, v, cb);
export const addContents = (db, v, cb) => addData(db, CONTENTS_STORAGE, v, cb);

호출부는 이렇게 단순해진다.

js
import { getAllTab, editContents } from '@util/IndexedDB';

getAllTab(userId, (tabs) => { /* ... */ });

얻은 것

원시 API유틸 래퍼
onsuccess 콜백 중첩.then() / await
매번 transaction·store 열기makePromise 한 곳
컴포넌트가 store 이름·키를 직접 다룸addTab(...) 도메인 함수

추가로 이 프로젝트에서는 저장할 때마다 레코드를 Blob으로 직렬화해 용량을 계산(new Blob([JSON.stringify(record)]).size)하고, 읽어온 데이터를 Redux로 미러링해 두었다. IndexedDB를 단일 진실 원본으로 두고 Redux는 동기 읽기용 캐시로 쓰는 구성이다.

비동기 대용량 저장소가 필요할 때 직접 래핑하는 대신 idb(약 1KB) 같은 라이브러리를 써도 된다. 위 패턴은 그 라이브러리가 내부에서 하는 일과 거의 같다.

무엇을 언제 쓰나

상황추천
인증 세션/토큰HttpOnly 쿠키 (XSS에 강함)
가벼운 UI 상태(테마 등)localStorage
탭 한정 임시 상태sessionStorage
큰 데이터·오프라인·바이너리IndexedDB

보안 — 클라이언트 저장은 "숨김"이 아니다

browser-secret-decoding에서 강조했듯, 클라이언트에 둔 데이터는 보호되지 않는다.

  • localStorage/sessionStorage/일반 쿠키는 JS로 다 읽힌다. XSS 취약점이 하나라도 있으면 거기 든 토큰은 탈취된다. 그래서 인증 토큰은 HttpOnly 쿠키가 권장된다.
  • 어느 저장소든 사용자가 DevTools로 열어 보고 수정할 수 있다. 민감 정보(비밀번호, 키, 타인의 개인정보)를 클라이언트에 저장하면 안 된다.
  • 저장소는 보안 경계가 아니라 편의 도구다. 신뢰가 필요한 검증은 서버에서 한다.

정리

  • 클라이언트 저장소는 용량·수명·서버 전송·동기 여부가 다르다. 그 차이로 용도가 갈린다.
  • localStorage/sessionStorage는 문자열 전용 동기 키-값 저장소다. 가벼운 UI 상태에 적합하고, 수명만 다르다.
  • 쿠키는 매 요청에 자동 전송되어 서버와 상태를 공유한다. HttpOnly/Secure/SameSite로 인증을 비교적 안전하게 담는다.
  • IndexedDB는 비동기 대용량 DB로, 구조화된 데이터와 바이너리, 오프라인에 쓴다.
  • 어느 것도 보안 저장소가 아니다. 민감 정보는 클라이언트에 두지 않는다.

참고 문서

Related Posts

같이 읽으면 좋은 글