Web Storage 와 IndexedDB

코린·2025년 8월 26일

Web Storage

HTML 혹은 Web Storage는 매번 HTTP 요청마다 서버에 요청을 보내는 것 없이 데이터를 사용자의 브라우저 저장소에 저장하는 것을 가능하게 합니다. 이는 성능 효율성을 높여주고 보안성을 향상시켰습니다.

HTML5 부터 Web Storage API가 표준으로 도입되었습니다. 이는 브라우저에서 키/값 쌍을 쿠키보다 훨씬 직관적으로 저장할 수 있는 방법을 제공합니다.

(쿠키는 문자열)

HTML5 이전에는 브라우저에 데이터를 클라이언트 측에 저장할 수 있는 수단이 cookie 밖에 없었습니다. (HTML5이 완전 표준화가 된 시기는 2014년 입니다.)

🍪 Cookie의 제약


(쿠키야...가지마...)

  • 4KB의 데이터 저장 제한
  • 쿠키는 매 HTTP 요청에 포함되어 있어 웹을 느려지게 하는 원인이 됨
  • HTTP 요청에 암호화 되지 않고 보내기 때문에 보안에 취약함
  • 쿠키는 사용자의 로컬에 텍스트로 저장 되어있어 쉽게 접근, 내용 확인이 가능

그렇다면 이제 영영 안녕...?

아닙니다!^^

여전히 쿠키는 사용합니다.

  • 세션 관리
    -> 로그인 상태 유지, 인증 토큰 전달 ..
  • 개인화
    -> 사용자 맞춤 경험 제공
  • 트래킹
    -> 광고/트래킹 목적

왜 쿠키가 없어지지 않을까?

  • HTTP 요청 헤더에 자동으로 포함되므로, 클라이언트 <-> 서버 간 인증 상태 유지에 유용함
  • 만료시간 기반 데이터 관리에 적합
    - Expires , Max-Age 속성을 지원하기 때문
  • 보안 속성 제공
    - HttpOnly 옵션을 주면 자바스크립트에서 접근할 수 없어 XSS 공격에 안전함
    - Secure , SameSite 옵션으로 HTTPS 전송만 허용, CSRF 공격을 막음

-> 보안 속성 실제로 확인해볼까요?

JSESSIONID 의 값이 저장되어 있고 Domain, Path 이 확인가능합니다.
위 예시의 경우

  • HttpOnly ✔️ : JS로 접근 불가
  • Secure ✔️ : HTTPS 연결에서만 전송
  • SameSite "None" : cross-site 요청에도 무조건 쿠키 전송, 여러 도메인 간 리소스 공유가 가능함
    - "Strict": 완전히 "같은 사이트"에서만 쿠키 전송 허용, CSRF 방어 최상
    - "Lax": 안전한 HTTP 메서드 (GET, HEAD, OPTIONS, TRACE) 로 발생한 top-level 탐색(주소창 입력, 북마크 클릭,외부 링크 클릭)에는 쿠키 전송 ⭕️, POST 요청이나 iframe, AJAX 같은 cross-site 서브 요청에는 쿠키 전송 ❌
  • Expires/Max-Age "Session" : 만료 시간 따로 없음, 브라우저 닫으면 삭제

XSS 공격

-> 게시판이나 웹 메일 등에 자바 스크립트와 같은 스크립트 코드를 삽입 해 개발자가 고려하지 않은 기능이 작동하게 하는 치명적일 수 있는 공격

CSRF

-> Cross Site Request Forgery, 사이트간 요청 위조

Web Storage 의 개념

DOM 스토리지로 알려졌으며, 웹 브라우저에서 제공하는 표준 JavaScript API 입니다. 웹사이트가 쿠키와 유사하게 사용자 기기에 영구 데이터를 저장할 수 있도록 하지만, 용량이 훨씬 더 크고 HTTP 헤더로 정보를 전송하지 않습니다. 키와 값이 모두 문자열인 연관 배열 데이터 모델을 노출하기 때문에 쿠키보다 더 나은 프로그래밍 인터페이스를 제공합니다.

저장 크기

  • Safari 8 : 5MB
  • Firefox : 10MB
  • Google Chrome : 원본당 10MB
항목쿠키localStoragesessionStorage
용량약 4KB보통 5~10MB보통 5~10MB
서버 전송✅ 자동 포함❌ 안 됨❌ 안 됨
만료지정 가능(Expires/Max-Age)직접 삭제 전까지 영구브라우저/탭 닫을 때 삭제
보안 옵션HttpOnly, Secure, SameSite 등 제공없음 (JS로만 접근 가능)없음 (JS로만 접근 가능)
주요 용도세션 관리, 인증, 트래킹클라이언트 영구 데이터 (설정값, 캐시 등)일시적 데이터 (폼 데이터 등)

사용예시

sessionStorage

  1. 일시적인 입력값 저장
    • 사용자가 긴 폼을 작성 중일 때, 페이지 새로고침해도 값이 날아가지 않도록
  2. 탭 단위 상태 관리
    • 결제 프로세스 단계, 퀴즈 진행 상태 등 한 탭 안에서만 유효해야 하는 정보
  3. 로그인 직후 일시적 데이터
    • ex) 방금 로그인 한 사용자에게만 보여줄 팝업

localStorage

  1. 다크 모드 같은 UI 환경 설정 저장
    • 브라우저 껐다 켜도 다크모드 유지
  2. 최근 본 상품 목록 저장
    • 비로그인 시에도 "최근 본 상품" 구현 가능
  3. 캐싱 데이터
    • 뉴스 기사, 환율, 주식 차트 같은 데이터 API로 받은 후, 일정 시간 동안 localStorage에 저장하여 API 호출 줄이기

로컬 스토리지 저장은 영원히 되는걸까..?

사실 정확하게 말하자면 영구는 아닙니다.
-> 사용자가 직접 삭제하기 전까지! 라고 생각하면 됩니다.

  • 사용자가 브라우저 캐시/저장소 삭제
  • 특정 도메인/사이트 데이터 삭제
  • 브라우저 자체가 저장 공간 관리 차원에서 비우는 경우 (매우 드뭄)

하지만, 개발자가 만료일을 따로 지정하지 않고 위 세가지 경우가 발생하지 않는다면 영구적으로 저장된다고 봐야합니다.

그러면 로컬/세션 스토리지는 쿠키처럼 만료시간 속성이 없나요..?

네 없습니다!

따라서 개발자가 직접 로직으로 "만료"를 흉내내야 합니다..!

  1. 데이터 저장 시 만료시간을 함께 저장
function setWithExpiry(key, value, ttlMs) {
  const now = Date.now();
  const item = {
    value: value,
    expiry: now + ttlMs,
  };
  localStorage.setItem(key, JSON.stringify(item));
}
  1. 데이터 읽을 시, 만료시간 확인
function getWithExpiry(key) {
  const itemStr = localStorage.getItem(key);
  if (!itemStr) return null;

  const item = JSON.parse(itemStr);
  const now = Date.now();

  if (now > item.expiry) {
    // 만료되면 삭제
    localStorage.removeItem(key);
    return null;
  }
  return item.value;
}

React를 사용할 것 이라면..!

React 환경에서 localStorage/sessionStorage에 만료시간을 붙여서 관리해주는 유틸/라이브러리가 존재합니다.

  1. localstorage-with-expire

  2. expired-storage

근데 사실 둘 다 너무 오래되기도 했고...구현이 어려운 일도 아니기 때문에 굳이...? 써야할까하는 생각도 듭니다....

IndexedDB

브라우저 내에서 구조화된 데이터 저장을 위한 API 입니다. 비동기 트랜잭션 기반의 NoSQL 데이터 베이스 입니다.

  • 대용량, 구조화 데이터 저장 가능
  • 인덱스/트랜잭션 지원 -> 복잡한 앱 데이터 적합
  • PWA(오프라인 웹앱), 대용량 캐시, 검색 가능한 로컬 DB 용도로 사용

Web Storage vs IndexedDB

구분localStoragesessionStorageIndexedDB
데이터 구조문자열만 저장 가능 (객체는 JSON.stringify 필요)동일객체 포함 모든 구조화 데이터(객체, 배열, Blob, 파일 등)
수명영구 (사용자가 삭제 전까지 유지)탭/창 닫으면 삭제영구 (사용자가 삭제 전까지 유지)
용량 제한보통 5~10MB보통 5~10MB수백 MB~수 GB (브라우저/디스크 상황 따라 다름)
동기/비동기동기적 → UI 스레드 블로킹 가능동일비동기적 (Promise/이벤트 기반, UI 블로킹 없음)
탭 간 공유같은 도메인에서 모든 탭 공유❌ 탭/창 별도 관리같은 도메인에서 공유
검색 기능Key로만 조회 가능Key로만 조회 가능KeyPath/Index 기반 고급 검색 지원 (SQL의 WHERE처럼 범위 조회 가능)
트랜잭션❌ 없음❌ 없음✅ 지원 (읽기/쓰기 원자적 보장)
사용 난이도간단 (setItem/getItem)간단복잡 (DB 설계 필요, 이벤트 기반 API)
주요 용도- UI 환경 설정(테마, 다크모드)
- 자동 로그인 토큰(비추천)
- 최근 본 항목
- 폼 입력 임시 저장
- 결제 진행 단계
- 탭 단위 세션 상태
- 대규모 데이터 저장
- 오프라인 캐시(PWA)
- 이미지/파일/동영상 썸네일 저장
- 인덱스 검색 필요할 때

IndexedDB 실제 사용

DB 열기

  var db;
  var request = indexedDB.open("MyTestDatabase");
  request.onerror = function (event) {
    alert("Why didn't you allow my web app to use IndexedDB?!");
  };
  request.onsuccess = function (event) {
    db = request.result;
  };

open 요청은 데이터베이스를 즉시 열거나 즉시 트랜잭션을 하지 않습니다.
이벤트로 처리한 결과나 오류 값이 있는 IDBOpenDBRequest 객체를 반환합니다.
open 함수의 두번째 인자는 버전을 의미합니다.

SQL에서 CREATE DATABASE,USE database,ALTER TABLE(버전 변경 시) 를 실행하는 과정과 비슷합니다.

DB 버전 생성 또는 업데이트

// This event is only implemented in recent browsers
request.onupgradeneeded = function (event) {
  // Save the IDBDatabase interface
  var db = event.target.result;

  // Create an objectStore for this database
  var objectStore = db.createObjectStore("name", { keyPath: "myKey" });
};

name -> ObjectStore 이름 (테이블 이름이라고 생각하면 편합니다.)
KeyPath -> 저장되는 각 객체 안에서 기본키(Primary Key) 역할을 할 속성 이름을 지정한 것 입니다.

데이터베이스 구성

키 경로키 생성기설명
NoNo이 객체 저장소는 숫자 및 문자열과 같은 원시 값을 포함한 모든 종류의 값을 보유 할 수 있습니다. 새 값을 추가 할 때 마다 별도의 키 인수를 공급해야합니다.
YesNo이 객체 저장소는 JavaScript 객체만 포함 할 수 있습니다. 객체에는 키 경로와 같은 이름의 속성이 있어야합니다.
NoYes이 객체 저장소는 모든 종류의 값을 보유할 수 있습니다. 키가 자동으로 생성됩니다. 또한 특정 키를 사용하려는 경우 별도의 키 인수를 공급할 수 있습니다
YesYse이 객체 저장소는 JavaScript 객체만 포함 할 수 있습니다. 일반적으로 키가 자동으로 생성되고 생성된 키의 값은 키 경로와 동일한 이름을 가진 속성의 객체에 저장됩니다. 그러나 그러한 속성이 이미 존재하는 경우, 새로운 키를 생성하는 것이 아닌 속성의 값을 키로 사용됩니다.

예시)

// This is what our customer data looks like.
const customerData = [
  { ssn: "444-44-4444", name: "Bill", age: 35, email: "bill@company.com" },
  { ssn: "555-55-5555", name: "Donna", age: 32, email: "donna@home.org" },
];
const dbName = "the_name";

var request = indexedDB.open(dbName, 2);

request.onerror = function (event) {
  // Handle errors.
};
request.onupgradeneeded = function (event) {
  var db = event.target.result;

  // "customers" 라는 ObjectStore를 생성합니다.
  // "ssn" 이 Key Path 즉 PK 가 됩니다.
  var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });

  // name 필드에 인덱스 생성 (중복 허용)
  objectStore.createIndex("name", "name", { unique: false });

  // email 필드에 인덱스 생성 (중복 불가)
  objectStore.createIndex("email", "email", { unique: true });
  
  //store.index("email").get("a@naver.com") -> 이런식으로 검색 가능

  // ObjectStore 생성 완료 한 후 데이터를 넣습니다.
  objectStore.transaction.oncomplete = function (event) {
    // Store values in the newly created objectStore.
    var customerObjectStore = db
      .transaction("customers", "readwrite") // readonly, readwrite
      .objectStore("customers"); // transaction 안에서 실제로 조작할 대상을 꺼내옵니다.
    customerData.forEach(function (customer) {
      customerObjectStore.add(customer);
    });
  };
};

왜 굳이 2단계..?


const tx = db.transaction(["customers", "orders"], "readwrite");
const customersStore = tx.objectStore("customers");
const ordersStore = tx.objectStore("orders");

이렇게 하면 하나의 트랜잭션 안에서 customers와 orders에 동시에 작업 가능 → 중간에 실패하면 둘 다 롤백됨.

RDB로 비유해보면,

  • transaction() = BEGIN TRANSACTION;
  • objectStore("customers") = SELECT * FROM customers 같은 테이블 선택

먼저 “이 트랜잭션에서 어떤 테이블에 어떤 권한으로 접근할 건지”를 선언 (transaction)
그 다음에 “그 테이블을 실제로 핸들링할 핸들러”를 가져옴 (objectStore)

데이터 추가

var transaction = db.transaction("customers", "readwrite");

"customers" ObjectStore를 대상으로 하는 트랜잭션을 생성합니다.
"readwrite" 데이터를 수정 할 수 있는 권한을 설정합니다.
- 권한 종류: "readwrite", "readonly"

transaction.oncomplete = function (event) {
  alert("All done!");
};

transaction.onerror = function (event) {
  // Don't forget to handle errors!
};

트랜잭션 이벤트는 위와 같습니다.

추가(add)

var objectStore = transaction.objectStore("customers");
for (var i in customerData) {
  var request = objectStore.add(customerData[i]);
  request.onsuccess = function (event) {
    // event.target.result == customerData[i].ssn
  };
}

삭제(delete)

var request = db
  .transaction(["customers"], "readwrite")
  .objectStore("customers")
  .delete("444-44-4444");
request.onsuccess = function (event) {
  // It's gone!
};

조회(get,getAll)

var transaction = db.transaction(["customers"]);
var objectStore = transaction.objectStore("customers");
var request = objectStore.get("444-44-4444");
request.onerror = function (event) {
  // Handle errors!
};
request.onsuccess = function (event) {
  // Do something with the request.result!
  alert("Name for SSN 444-44-4444 is " + request.result.name);
};

get 으로 조회 시 PK 를 기준으로만 조회할 수 있습니다.
get: 1개의 값, (여러개일 경우 첫번째 값 1개만 반환)
getAll: 모든 값

수정(put)

var objectStore = db
  .transaction(["customers"], "readwrite")
  .objectStore("customers");
var request = objectStore.get("444-44-4444");
request.onerror = function (event) {
  // Handle errors!
};
request.onsuccess = function (event) {
  // Get the old value that we want to update
  var data = event.target.result;

  // update the value(s) in the object that you want to change
  data.age = 42;

  // Put this updated object back into the database.
  var requestUpdate = objectStore.put(data);
  requestUpdate.onerror = function (event) {
    // Do something with the error
  };
  requestUpdate.onsuccess = function (event) {
    // Success - the data is updated!
  };
};

cursor 와 index

PK가 아닌 다른 필드로 빠르게 검색할 때 필요한 것이 index
PK 또는 특정 값으로 하나만 바로 가져오는 것이 아닌 "여러 개의 데이터를 순회"하거나 "조건에 맞는 범위 데이터"를 가져올 때는 cursor 가 필요합니다.

var tx = db.transaction("customers");
var store = tx.objectStore("customers");
var index = store.index("name");

var request = index.openCursor(IDBKeyRange.only("Alice")); //IDBKeyRange 는 IndexedDB API에서 제공하는 전역 객체
request.onsuccess = function (event) {
  var cursor = event.target.result;
  if (cursor) {
    console.log("Alice 데이터:", cursor.value);
    cursor.continue();
  }
};

IDBKeyRange -> IndexedDB의 범위 조건 객체입니다. only, lowerBound, upperBound, bound 메서드로 범위를 지정합니다.

// "Donna"만을 조회
var singleKeyRange = IDBKeyRange.only("Donna");

// "Bill"을 포함한, "Bill" 이후 모든 값을 조회
var lowerBoundKeyRange = IDBKeyRange.lowerBound("Bill");

// "Bill"을 제외한, "Bill" 다음 모든 값을 조회
var lowerBoundOpenKeyRange = IDBKeyRange.lowerBound("Bill", true);

// "Donna"를 제외한, 이전 모든 값을 조회
var upperBoundOpenKeyRange = IDBKeyRange.upperBound("Donna", true);

// "Donna"를 제외한, "Bill"과 "Donna" 사이 모든 값을 조회
var boundKeyRange = IDBKeyRange.bound("Bill", "Donna", false, true);

// 위 키 범위 중 하나를 사용하려면, openCursor()/openKeyCursor()에 첫 번째 인자로 넘겨주세요.
index.openCursor(boundKeyRange).onsuccess = function (event) {
  var cursor = event.target.result;
  if (cursor) {
    // 조회된 값으로 무언가 수행한다.
    cursor.continue();
  }
};

근데 혹시 사용자가 예전버전 디비를 사용하고 있는중이라면.....??

IndexedDB는 한 버전의 스키마만 유지해야 합니다.
탭A가 버전1 DB를 열고 있는 중인데, 탭B에서 버전2로 열려고 하면 -> 충돌이 발생합니다.

var openReq = mozIndexedDB.open("MyTestDatabase", 2);

openReq.onblocked = function (event) {
  // 버전이 다른 DB를 사용하는 탭을 열었을 경우, 닫도록 요청합니다.
  alert("Please close all other tabs with this site open!");
};

openReq.onupgradeneeded = function (event) {
  // 다른 탭이 모두 닫혀서 DB 업그레이드가 가능해질 때 DB를 세팅합니다.
  db.createObjectStore(/* ... */);
  useDatabase(db);
};

//DB 정상적으로 실행
openReq.onsuccess = function (event) {
  var db = event.target.result;
  useDatabase(db);
  return;
};

function useDatabase(db) {
  // 핸들러를 추가하여 서로 다른 버전 DB를 업그레이드하지 않도록 해야합니다.
  // 탭을 닫기 전까지는, 업그레이드가 block 상태로 계속 대기합니다.
  // db.close() 하고 "새로고침 필요" 메세지를 띄웁니다.
  db.onversionchange = function (event) {
    db.close();
    alert("A new version of this page is ready. Please reload!");
  };

  // Do stuff with the database.
}

보안

IndexedDB는 same-origin policy를 따릅니다. (web Storage 도 동일합니다!)
같은 프로토콜 + 도메인 + 포트 조합에서만 DB를 공유합니다.
예시)

다만,
<iframe>으로 다른 도메인(origin)의 콘텐츠를 넣었을 때는 IndexedDB에 접근이 가능합니다.

예시)

<!-- 메인 페이지 origin: https://main.com -->
<iframe src="https://ads.com/ad.html"></iframe>

https://ads.com/ad.html 이 페이지는 자기 origin(ads.com) 기준의 IndexedDB에 접근할 수 있습니다.
다만, main.comads.com이 DB를 서로 공유하는 것은 아니고, 각자 자기 DB만 접근합니다.

마무리하며

Web Storage 와 IndexedDB 그리고 쿠키..(왠지 이름이 맛있어보여서 정감이 갑니다)를 각각 상황에 맞게 잘 쓰는것이 중요하다고 생각합니다.

IndexedDB는 실제로 사용해 본 적은 없지만 오늘을 계기로 조만간 사용해 볼 예정입니다!

그렇다면...

감사합니다!

참고

Web Storage API
HTML Web Storage API
localStorage와 sessionStorage
Web Storage
Web Storage API in HTML5
Introduction to HTML Storage

HTTP 쿠키

IndexedDB 사용하기
IndexedDB API
[TS] IndexedDB 공통 유틸: 구현과 사용법 (+ 기본 개념)

profile
안녕하세요 코린입니다!

0개의 댓글