1. 클라이언트에도 데이터베이스가 있다?
웹은 기본적으로 Stateless(무상태)입니다. 서버는 요청이 끝나면 당신이 누구였는지 잊어버립니다. 그래서 "로그인 유지"나 "장바구니" 같은 기능을 구현하려면 어딘가에 데이터를 저장해야 합니다. 옛날엔 쿠키(Cookie) 하나뿐이었지만, 지금은 브라우저 안에 거대한 NoSQL DB(IndexedDB)까지 들어있습니다.
선택지가 많아진 만큼 개발자의 고민도 깊어집니다. "JWT 토큰은 어디에 저장해야 안전할까요?" "장바구니 데이터는 브라우저를 껐다 켜도 남아있어야 하는데, 뭘 써야 하죠?"
이 질문에 대한 답을 찾아봅시다.
2. 저장소 4대장 완벽 비교
각 저장소의 특징과 용도를 명확히 구분할 줄 알아야 합니다.
2.1. Cookie (쿠키) - "가장 오래된 녀석"
- 용량: 4KB (매우 작음).
- 수명: 만료 날짜 지정 가능. (지정 안 하면 세션 쿠키).
- 특징: 모든 HTTP 요청 헤더에 자동으로 실려서 서버로 전송됨.
- 용도: 서버가 "너 로그인했니?" 확인할 때 (세션 ID, 토큰).
- 보안:
HttpOnly,Secure플래그로 보호 가능.
2.2. Web Storage (Local & Session) - "간편한 키-밸류"
HTML5에서 등장한, 쿠키의 단점을 보완하기 위한 저장소입니다.
- Local Storage:
- 수명: 영구적(Permanent). 브라우저를 끄거나 컴퓨터를 재부팅해도 남아있습니다. 사용자가 지우거나 스크립트로 지워야 사라집니다.
- 용도: 다크모드 설정, "오늘 하루 열지 않기", 자동 로그인된 아이디 등.
- Session Storage:
- 수명: 휘발성(Volatile). 탭(Tab) 단위로 격리됩니다. 탭을 닫으면 데이터가 즉시 삭제됩니다.
- 용도: 은행 사이트의 로그인 정보(보안), 입력 중인 폼 데이터(실수로 새로고침했을 때 복구용).
- 특징: 5~10MB 용량. 서버로 전송되지 않음. 동기(Synchronous) API라 대용량 처리 시 메인 스레드 멈춤(Blocking) 발생 주의.
2.3. IndexedDB - "브라우저 속의 NoSQL"
- 용량: 하드디스크 남은 용량의 50~80%까지 가능 (수 GB).
- 특징: 비동기(Asynchronous) API. 트랜잭션 지원. 인덱스(Index) 지원.
- 용도: 구글 독스 같은 오프라인 웹앱(PWA), 이미지 캐싱, 리덕스(Redux) 상태 영구 저장, 복잡한 데이터 검색.
2.4. Cache API - "네트워크 요청 저장소"
- 용도: Service Worker와 짝꿍. HTTP 요청(Request)과 응답(Response) 객체 자체를 저장.
- 특징: 오프라인 모드 구현의 핵심.
3. 심화: Web Storage의 숨겨진 디테일
3.1 SessionStorage의 '탭 격리'
많은 개발자가 오해하는 부분입니다.
- LocalStorage: 모든 탭과 창에서 공유됩니다. A탭에서 저장하면 B탭에서도 보입니다.
- SessionStorage: 같은 사이트라도 탭이 다르면 데이터가 공유되지 않습니다.
- 하지만!
window.open()으로 자식 탭을 열면, 부모 탭의 SessionStorage 데이터가 복사(Cloning)되어 전달됩니다. (연동되는 게 아니라 초기값만 복사됨).
3.2 Storage Event: 탭 간 통신
LocalStorage의 값이 변경되면, 같은 도메인의 다른 탭들에 storage 이벤트가 발생합니다.
이를 이용해 "A탭에서 로그아웃하면 B탭도 같이 로그아웃"되거나, "설정 변경 시 모든 탭에 즉시 반영"하는 기능을 구현할 수 있습니다.
// 다른 탭에서 localStorage를 변경했을 때 실행됨
window.addEventListener('storage', (event) => {
if (event.key === 'theme') {
applyTheme(event.newValue); // 즉시 테마 변경 적용
}
if (event.key === 'token' && event.newValue === null) {
alert('로그아웃되었습니다.');
window.location.reload();
}
});
이 이벤트는 값을 변경한 당사자 탭(Self)에서는 발생하지 않습니다. 오직 다른 탭에서만 발생합니다.
4. 심화: Cookie의 보안 속성 (Flags)
쿠키는 그냥 심으면 털립니다. 보안 엔지니어처럼 설정해야 합니다.
- HttpOnly:
- 가장 중요합니다. 자바스크립트(
document.cookie)로 쿠키에 접근하는 것을 막습니다. - XSS(교차 사이트 스크립팅) 공격을 당해도 해커가 토큰을 훔쳐갈 수 없게 만듭니다.
- 가장 중요합니다. 자바스크립트(
- Secure:
- HTTPS(암호화된 통신)일 때만 쿠키를 전송합니다.
- 공공 와이파이 같은 곳에서 패킷 스니핑(Sniffing)을 당해도 쿠키 노출을 막습니다.
- SameSite:
- CSRF(교차 사이트 요청 위조) 공격을 방어합니다.
Strict: 내 사이트 안에서만 쿠키 전송. 외부 링크 타고 들어오면 쿠키 안 보냄(로그인 풀림).Lax(기본값): 외부에서 들어올 때(Link Navigation)는 허용하지만,<img>태그나<iframe>,POST요청 등에는 쿠키를 안 보냄. 대부분의 사이트에 적합.None: 다 보냄. (단,Secure필수로 켜야 함).
5. IndexedDB: 브라우저 속의 괴물
IndexedDB는 단순한 키-밸류 저장소가 아닙니다. Transactional NoSQL Database입니다. LocalStorage가 5MB짜리 메모장이라면, IndexedDB는 수백 MB를 다루는 엑셀 파일이나 같습니다.
5.1. 핵심 개념
- Database: 여러 개의 Object Store를 담는 컨테이너. 버전(Version) 관리가 됩니다.
- Object Store: RDBMS의 Table, MongoDB의 Collection과 같습니다.
- Transaction: 데이터 무결성을 보장합니다. "읽기 전용(readonly)"과 "읽기 쓰기(readwrite)" 트랜잭션이 있습니다. 트랜잭션 도중 에러가 나면 전체 롤백됩니다.
- Cursor: 데이터를 하나씩 순회할 때 사용합니다.
- Index: 특정 필드(예:
price,date)로 빠르게 검색하기 위해 인덱스를 걸 수 있습니다.
5.2. 코드로 보는 사용법 (Native vs Library)
네이티브 API는 이벤트 기반(onsuccess, onerror)이라 콜백 지옥에 빠지기 쉽습니다. 실제로는 Dexie.js 같은 래퍼 라이브러리를 쓰는 게 정신 건강에 좋습니다.
/* Native API (복잡함) */
const req = indexedDB.open("MyStore", 1);
req.onupgradeneeded = (e) => {
const db = e.target.result;
db.createObjectStore("users", { keyPath: "id" });
};
/* Dexie.js (깔끔함) */
import Dexie from 'dexie';
const db = new Dexie("MyStore");
db.version(1).stores({
users: "++id, name, age" // id는 자동증가, name과 age는 인덱싱
});
// 데이터 추가 (Async/Await 사용 가능!)
await db.users.add({ name: "Ratia", age: 25 });
// 데이터 조회 (쿼리 빌더 지원)
const adults = await db.users.where('age').above(19).toArray();
6. 가이드: 쇼핑몰 장바구니 패턴
"장바구니는 어디에 담아야 할까요?" 이 질문 하나로 주니어와 시니어를 가를 수 있습니다.
6.1. 하수: LocalStorage에 JSON.stringify
const cart = [{ id: 1, name: "맥북", price: 3000000 }];
localStorage.setItem('cart', JSON.stringify(cart));
- 문제점:
- Blocking: 상품이 1000개면
stringify할 때마다 화면 버벅임(Frame Drop). - 용량 부족: 5MB 넘으면
QuotaExceededError발생하며 저장 실패. - 데이터 깨짐: 여러 탭에서 동시에 장바구니를 수정하면 데이터가 덮어씌워짐(Race Condition).
- Blocking: 상품이 1000개면
6.2. 고수: IndexedDB + Redux Persist (혹은 Recoil/Zustand)
쇼핑몰 장바구니나 텍스트 에디터의 임시 저장 기능처럼 데이터 무결성과 대용량이 필요한 경우 무조건 IndexedDB입니다. IndexedDB는 비동기(Non-blocking)라서 UI 스레드를 방해하지 않고, 트랜잭션 덕분에 데이터 꼬임도 방지합니다.
7. 저장소 용량 관리 (Quota Management)
브라우저는 저장 공간을 무제한으로 주지 않습니다.
7.1 얼마까지 쓸 수 있나?
- 크롬: 디스크 여유 공간의 약 80%까지. (단, 단일 오리진당 제한 있음).
- 사파리: 훨씬 짭니다(수백 MB 수준). 그리고 7일 동안 방문 안 하면 데이터를 싹 지워버리는 정책(ITP)이 있어 주의해야 합니다.
7.2 영구 저장 요청 (Persistent Storage)
브라우저는 디스크가 꽉 차면 LRU(Least Recently Used) 알고리즘으로 안 쓰는 사이트 데이터부터 지웁니다. 내 사이트 데이터가 지워지면 안 된다면, 브라우저에게 "이 데이터는 중요해!"라고 신고해야 합니다.
if (navigator.storage && navigator.storage.persist) {
const isPersisted = await navigator.storage.persist();
console.log(`영구 저장 허용됨: ${isPersisted}`);
}
허용되면, 사용자가 직접 지우기 전까지는 브라우저가 임의로 청소하지 않습니다.
8. 마무리: 무엇을 써야 할까?
상황별 요약 가이드입니다.
- JWT (Access Token):
- HttpOnly Cookie (1순위). XSS 방어에 유리.
- 부득이하다면 메모리 변수(새로고침하면 로그아웃됨)에 넣고, Refresh Token만 HttpOnly Cookie에.
- LocalStorage 저장은 되도록 피하세요. (XSS 뚫리면 끝장).
- 자동 로그인 여부 / 테마(Dark Mode):
- LocalStorage. 단순 설정값은 여기가 딱입니다.
- 세션 데이터 (은행 보안 카드 입력 정보):
- SessionStorage. 탭 닫으면 날아가야 안전합니다.
- 장바구니 / 오프라인 캐시 / 대용량 데이터:
- IndexedDB. 성능과 용량 모두 잡으려면 선택이 아닌 필수입니다.
- 이미지, CSS, JS 파일 캐싱:
- Cache API (Service Worker). PWA의 영역입니다.
브라우저는 더 이상 단순한 문서 뷰어가 아닙니다. 하나의 작은 운영체제입니다. 각 저장소의 특성을 이해하고 적재적소에 배치하는 것이 프론트엔드 엔지니어의 핵심 역량입니다.