로컬 스토리지와 세션 스토리지
문자열 키·값을 저장하는 Web Storage API를 익히고 localStorage·sessionStorage·쿠키의 수명과 전송 차이를 비교합니다.
지난 절에서는 웹 애플리케이션이 Fetch API를 통해 서버와 비동기적으로 통신하는 방법을 다뤘습니다.
서버에서 데이터를 받아 화면에 반영하는 흐름은 중요하지만, 상황에 따라서는 서버를 거치지 않고 클라이언트(브라우저) 내부에 데이터를 저장해야 합니다.
대표적으로 로그인 상태 유지, 설정 정보 저장, 장바구니 임시 보관, 오프라인 지원 같은 기능이 여기에 해당합니다.
과거에는 이런 클라이언트 저장 용도로 쿠키(Cookie)를 많이 사용했습니다.
하지만 쿠키는 용량 제한(약 4KB), 도메인·경로·보안 정책에 맞는 HTTP 요청에 자동 전송되는 오버헤드, 다루기 불편한 API 같은 한계가 있습니다.
HTML 표준의 웹 스토리지(Web Storage) API는 이 한계를 보완하며, 더 큰 저장 용량과 단순한 API를 제공합니다.
웹 스토리지 API는 크게 두 가지 유형을 제공합니다.
로컬 스토리지(Local Storage)와 세션 스토리지(Session Storage).
이 글에서는 두 스토리지의 특징과 사용법, 그리고 실제 적용 시나리오를 순서대로 정리합니다.
DECISION TREE · STORAGE SECURITY & LIFETIME
브라우저 저장소 선택 계약
민감한 원문과 권한 판단은 서버에 둔다. 브라우저가 저장할 비민감 값은 요청 전송·탭 수명·조회 방식에 따라 저장소를 고른다.
각 예는 해당 저장소로 종결하고 아니오는 다음 질문으로 이어집니다. Cookie의 연결값과 서버의 권한 검증, JavaScript가 읽는 비민감 저장 값을 구별합니다. Cookie 속성 하나만으로 모든 요청 보안이 완성되지는 않습니다.
Same Origin은 XSS로부터 값을 숨겨 주지 않는다. sessionStorage/localStorage/IndexedDB는 JavaScript가 읽을 수 있으므로 민감 원문을 저장하지 않고, 보안·수명·조회 방식 순으로 선택한다. localStorage는 명시적 만료가 없지만 사용자 삭제·브라우저 정책으로 지워질 수 있다.
웹 스토리지 (Web Storage) 개요
웹 스토리지는 클라이언트 측에서 key-value 형태로 데이터를 저장할 수 있게 해주는 웹 API입니다.
이 API는 동일 출처(Same Origin) 정책을 따르며, 각 출처(도메인, 프로토콜, 포트)마다 별도의 저장 공간을 가집니다.
즉, example.com에서 저장한 데이터는 another.com에서 접근할 수 없습니다.
두 가지 주요 웹 스토리지 객체는 전역 Window 객체의 프로퍼티로 제공됩니다.
window.localStoragewindow.sessionStorage
이 두 객체는 동일한 메서드와 프로퍼티를 제공하지만, 데이터의 지속성(persistence)에서 차이가 있습니다.
앞의 브라우저 저장소 선택 계약 다이어그램은 저장 위치를 고를 때 먼저 확인해야 하는 기준을 요약합니다.
로컬 스토리지 (Local Storage)
로컬 스토리지는 브라우저를 닫았다가 다시 열어도 데이터가 유지되는 명시적인 만료 시간이 없는 저장소입니다.
사용자·브라우저의 사이트 데이터 삭제나 저장 정책에 따라 제거될 수 있으므로 영구 보존을 보장하지는 않습니다.
- 지속성: 명시적 만료 없음 (일반적으로 종료 후 유지)
- 만료 기간: 없음 (수동 삭제 필요)
- 용량: 출처당 약 5MB ~ 10MB (브라우저마다 다름)
- 접근성: 동일 출처의 모든 탭/창에서 접근 가능
- 데이터 형식: 문자열(
string)만 저장 가능
로컬 스토리지 사용법
localStorage 객체는 setItem(), getItem(), removeItem(), clear(), key() 등의 메서드를 제공합니다.
localStorage.setItem(key, value)key: 데이터를 식별할 키 (문자열)value: 저장할 값 (문자열)value는 항상 문자열로 변환되어 저장됩니다. 객체나 배열을 저장하려면JSON.stringify()를 사용하여 문자열로 변환해야 합니다.
// 문자열 저장
localStorage.setItem('username', 'Alice');
localStorage.setItem('theme', 'dark');
// 객체 저장 (JSON.stringify 사용)
const userSettings = {
notifications: true,
language: 'ko'
};
localStorage.setItem('settings', JSON.stringify(userSettings));
console.log("로컬 스토리지에 데이터 저장됨.");localStorage.getItem(key)key: 조회할 데이터의 키- 저장된 문자열 값을 반환합니다. 값이 없으면
null을 반환합니다. - 저장했던 객체나 배열은
JSON.parse()를 사용하여 다시 객체나 배열로 변환해야 합니다.
const storedUsername = localStorage.getItem('username');
console.log("저장된 사용자 이름:", storedUsername); // 결과: Alice
const storedTheme = localStorage.getItem('theme');
console.log("저장된 테마:", storedTheme); // 결과: dark
// 객체 조회 (JSON.parse 사용)
const storedSettings = localStorage.getItem('settings');
if (storedSettings) {
const parsedSettings = JSON.parse(storedSettings);
console.log("저장된 설정:", parsedSettings); // 결과: {notifications: true, language: 'ko'}
}localStorage.removeItem(key)key: 삭제할 데이터의 키
localStorage.removeItem('theme');
console.log("테마 데이터 삭제됨.");
console.log("남은 사용자 이름:", localStorage.getItem('username')); // 결과: Alice
console.log("삭제된 테마:", localStorage.getItem('theme')); // 결과: nulllocalStorage.clear()- 해당 출처에 저장된 모든 로컬 스토리지 데이터를 삭제합니다.
localStorage.clear();
console.log("모든 로컬 스토리지 데이터 삭제됨.");
console.log("사용자 이름:", localStorage.getItem('username')); // 결과: nulllocalStorage.key(index) 또는 for...inlocalStorage.length로 저장된 항목의 개수를 알 수 있습니다.
localStorage.setItem('fruit', 'apple');
localStorage.setItem('drink', 'water');
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const value = localStorage.getItem(key);
console.log(`${key}: ${value}`);
}
/*
결과 예시:
fruit: apple
drink: water
*/
// 또는 for...in 루프 (비권장: 프로토타입 체인의 속성도 포함될 수 있음)
// for (let key in localStorage) {
// if (localStorage.hasOwnProperty(key)) {
// console.log(`${key}: ${localStorage.getItem(key)}`);
// }
// }로컬 스토리지 활용 시나리오
- 사용자 설정 저장: 테마(다크 모드/라이트 모드), 언어 설정, 알림 설정 등
- 로그인 화면의 비민감 설정: 마지막으로 선택한 표시 옵션 등. 인증 토큰을 저장하면 같은 출처의 JavaScript가 읽을 수 있으므로, 인증 설계에서는 XSS 노출과 HttpOnly 쿠키 등 대안을 검토해야 합니다.
- 장바구니 정보(오프라인 유지): 사용자가 브라우저를 닫아도 장바구니 내용 유지
- 캐싱 데이터: 자주 사용되는 데이터를 클라이언트에 저장하여 서버 요청 감소 (API 응답 캐싱)
세션 스토리지 (Session Storage)
세션 스토리지는 로컬 스토리지와 동일한 API를 제공하지만, 데이터가 현재 브라우저 세션 동안만 유지됩니다.
즉, 사용자가 탭이나 브라우저 창을 닫으면 해당 세션 스토리지의 데이터는 자동으로 삭제됩니다.
- 지속성: 세션 동안만 유지 (탭/창 닫으면 삭제)
- 만료 기간: 세션 종료 시 (만료 기간 설정 불가)
- 용량: 출처당 약 5MB ~ 10MB (브라우저마다 다름)
- 접근성: 동일한 탭/창 내에서만 접근 가능 (다른 탭/창에서는 접근 불가, 새로고침은 유지)
- 데이터 형식: 문자열(
string)만 저장 가능
세션 스토리지 사용법
로컬 스토리지와 동일한 메서드를 사용합니다.
localStorage 대신 sessionStorage 객체를 사용하면 됩니다.
// 데이터 저장
sessionStorage.setItem('tempMessage', '이 메시지는 현재 세션 동안만 유지됩니다.');
sessionStorage.setItem('lastVisitedPage', '/products/detail/123');
// 데이터 조회
const message = sessionStorage.getItem('tempMessage');
console.log("세션 저장 메시지:", message);
// 데이터 삭제
sessionStorage.removeItem('lastVisitedPage');
sessionStorage.clear(); // 현재 세션의 모든 데이터 삭제세션 스토리지 활용 시나리오
- 단일 세션 내 임시 데이터: 폼 입력값 임시 저장 (페이지 이동 후 다시 돌아왔을 때), 일회성 알림 메시지
- 단계별 마법사(Wizard) 형식의 데이터: 여러 단계로 이루어진 회원가입 폼에서 각 단계의 입력 데이터를 다음 단계로 넘기기 전까지 임시 저장
- 세션별 사용자 액션 추적: 특정 세션에서만 필요한 사용자 행동 로그
쿠키 (Cookie)와 웹 스토리지 비교 (간략)
로컬 스토리지와 세션 스토리지에서는 저장 위치, 요청 흐름, 화면 전환 결과를 연결합니다.
| 특징 | 쿠키 (Cookie) | 로컬 스토리지 (Local Storage) | 세션 스토리지 (Session Storage) |
|---|---|---|---|
| 지속성 | 만료일 지정 가능 (영구적/세션) | 명시적 만료 없음 (일반적으로 종료 후 유지) | 세션 동안만 유지 (탭/창 닫으면 삭제) |
| 용량 | 약 4KB | 5~10MB | 5~10MB |
| 서버 전송 | 조건에 맞는 HTTP 요청에 자동 전송 | 클라이언트에서만 접근 가능 (서버로 자동 전송 안 됨) | 클라이언트에서만 접근 가능 (서버로 자동 전송 안 됨) |
| 접근성 | 동일 출처의 모든 탭/창, 서버에서 접근 가능 | 동일 출처의 모든 탭/창에서 접근 가능 | 동일 출처의 동일 탭/창에서만 접근 가능 |
| API | 복잡 (document.cookie 문자열 파싱) | 간단 (setItem, getItem 등) | 간단 (setItem, getItem 등) |
| 주요 용도 | 사용자 인증 (세션 ID), 추적 | 사용자 설정, 캐시, 비민감 오프라인 데이터 | 폼 데이터, 임시 상태, 일회성 메시지 |
문자열 설정 저장에는 Web Storage가 편리하고, 서버로 자동 전송하거나 HttpOnly가 필요한 값에는 쿠키가 적합합니다. 용량·수명·접근 주체에 따라 선택합니다.
쿠키는 주로 서버에서 읽고 써야 하는 사용자 인증(세션 ID)이나 트래킹 정보 등에 제한적으로 사용됩니다.
웹 스토리지는 만료 시간을 직접 관리하지 않으므로, 저장 값 안에 버전과 만료 기준을 함께 넣어두면 오래된 데이터가 화면을 망치는 일을 줄일 수 있습니다.
아래 다이어그램은 저장, 복원, 정리, 탭 동기화까지 한 흐름으로 확인하는 기준입니다.
저장 envelope 검증 수명 — 상태 추적
readTheme의 raw 값→JSON→version/shape→expiry 검사에서 선택한 theme와 제거 여부가 바뀐다. 접근 실패·없음·invalid/expired·fresh를 실제 readTheme 계약에 맞춰 추적한다.
STATE · WEB PLATFORM CONTRACT
저장 envelope 검증 수명
저장한 JSON에 버전·만료·데이터를 함께 넣고, 복원 전에 문법과 구조와 만료를 검증한다. 실패한 값은 제거하고 기본값으로 돌아간다.
아래 복원 그림은 새 readTheme 실습입니다. 접근 실패·값 없음은 기본값으로, 문법·버전·형태·만료 실패는 자기 키의 제거 시도 뒤 기본값으로 갑니다. removed:false도 기본값 복원과 함께 관찰해야 합니다.
JSON.parse의 성공만으로 구조·버전·만료가 검증되지는 않는다. Web Storage는 expiresAt을 자동 적용하지 않으므로 앱이 복원할 때 판단한다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 저장 읽기 | 저장: raw 또는 없음 / fallback: light | getItem의 접근 실패/없음/문자열을 구별한다 | 접근 불가와 값 부재는 다르다 |
| 검증 | 검사: 형식·버전·값·유효기간 / invalid: 삭제 시도 | JSON/version1/theme/expiry를 확인한다 | 부분적으로 파싱됐다는 이유로 신뢰하지 않는다 |
| 읽기 결과 | fresh: 유효 theme / invalid/expired: light | fresh theme을 쓰거나 light 및 정리 성공 여부를 반환한다 | 삭제 성공과 fallback 결정은 별도 관찰이다 |
저장 envelope 검증 수명 — 상태 추적
readTheme의 raw 값→JSON→version/shape→expiry 검사에서 선택한 theme와 제거 여부가 바뀐다. 접근 실패·없음·invalid/expired·fresh를 실제 readTheme 계약에 맞춰 추적한다.
STATE · WEB PLATFORM CONTRACT
저장 envelope 검증 수명
저장한 JSON에 버전·만료·데이터를 함께 넣고, 복원 전에 문법과 구조와 만료를 검증한다. 실패한 값은 제거하고 기본값으로 돌아간다.
아래 복원 그림은 새 readTheme 실습입니다. 접근 실패·값 없음은 기본값으로, 문법·버전·형태·만료 실패는 자기 키의 제거 시도 뒤 기본값으로 갑니다. removed:false도 기본값 복원과 함께 관찰해야 합니다.
JSON.parse의 성공만으로 구조·버전·만료가 검증되지는 않는다. Web Storage는 expiresAt을 자동 적용하지 않으므로 앱이 복원할 때 판단한다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| 저장 읽기 | 저장: raw 또는 없음 / fallback: light | getItem의 접근 실패/없음/문자열을 구별한다 | 접근 불가와 값 부재는 다르다 |
| 검증 | 검사: 형식·버전·값·유효기간 / invalid: 삭제 시도 | JSON/version1/theme/expiry를 확인한다 | 부분적으로 파싱됐다는 이유로 신뢰하지 않는다 |
| 읽기 결과 | fresh: 유효 theme / invalid/expired: light | fresh theme을 쓰거나 light 및 정리 성공 여부를 반환한다 | 삭제 성공과 fallback 결정은 별도 관찰이다 |
스토리지 활용 요약
이 절에서는 로컬 스토리지(Local Storage)와 세션 스토리지(Session Storage)의 차이를 정리했습니다.
핵심은 지속성 범위입니다.
로컬 스토리지는 브라우저를 다시 열어도 유지되고, 세션 스토리지는 현재 탭/세션 범위에서만 유지됩니다.
또한 두 스토리지가 setItem(), getItem(), removeItem(), clear() 중심의 동일한 API를 제공하지만, 저장 단위가 문자열이라는 제약이 있다는 점도 확인했습니다.
그래서 객체나 배열을 저장할 때는 JSON.stringify()와 JSON.parse()를 함께 사용해야 합니다.
쿠키와 비교하면 웹 스토리지는 저장 용량과 API 사용성이 더 좋고, HTTP 요청마다 데이터가 자동 전송되지 않아 불필요한 트래픽도 줄일 수 있습니다.
다음 절에서는 History API로 URL과 화면 전환을 관리하는 방법을 살펴보겠습니다. 따라하기: 만료는 저장소가 아니라 읽는 코드가 판단합니다
아래 확장은 비민감 화면 설정 하나만 다룹니다. 초기 저장·삭제 예제는 쓰기와 삭제가 허용된 전용 학습 출처에서 실행합니다. 저장 키는 wb9.theme, 데이터는 light 또는 dark, 버전은 1입니다. expiresAt과 현재 시각은 밀리초 정수로 비교하고, 현재 시각이 만료 시각과 같으면 만료로 판정합니다. 이 숫자를 저장한다고 localStorage가 해당 시각에 자동 삭제하지는 않습니다.
const THEME_KEY = 'wb9.theme';
function removeTheme(storage) {
try { storage.removeItem(THEME_KEY); return true; }
catch { return false; }
}
function readTheme(storage, now) {
let raw;
try { raw = storage.getItem(THEME_KEY); }
catch { return { state: 'unavailable', theme: 'light' }; }
if (raw === null) return { state: 'absent', theme: 'light' };
let saved;
try { saved = JSON.parse(raw); }
catch {
return { state: 'invalid', theme: 'light', removed: removeTheme(storage) };
}
const valid = saved !== null && typeof saved === 'object'
&& saved.version === 1 && Number.isSafeInteger(saved.expiresAt)
&& saved.data !== null && typeof saved.data === 'object'
&& (saved.data.theme === 'light' || saved.data.theme === 'dark');
if (!valid) {
return { state: 'invalid', theme: 'light', removed: removeTheme(storage) };
}
if (now >= saved.expiresAt) {
return { state: 'expired', theme: 'light', removed: removeTheme(storage) };
}
return { state: 'fresh', theme: saved.data.theme };
}
function restoreTheme(now) {
try { return readTheme(window.localStorage, now); }
catch { return { state: 'unavailable', theme: 'light' }; }
}
localStorage.setItem(THEME_KEY, JSON.stringify({
version: 1, expiresAt: 2000, data: { theme: 'dark' }
}));
console.log(readTheme(localStorage, 1000)); // fresh, dark
console.log(readTheme(localStorage, 2000)); // expired, light; 이 키 제거 시도
console.log(readTheme(localStorage, 2001)); // 제거 성공한 저장소라면 absent, light먼저 저장 값이 있는지, 그다음 JSON 문법과 현재 버전의 형태를 검사합니다. 마지막에 유효 기간을 확인해야 dark를 복원할 수 있습니다. 예제의 작은 시각은 경계를 재현하기 위한 가상 시각입니다. 실제 복원에는 restoreTheme(Date.now())처럼 같은 시간 단위를 사용합니다. restoreTheme의 try는 localStorage 객체를 얻을 때의 예외도 처리하고, readTheme은 얻은 객체의 getItem 예외를 처리합니다. 저장소 접근이나 삭제가 허용되지 않을 수도 있으므로 unavailable과 removed: false를 정상 복원과 구별합니다. 저장 성공·실패 처리는 쓰기 쪽에서도 별도로 확인합니다.
앞의 clear()는 이 출처의 저장소 전체를 지웁니다. 위 코드는 자기 설정 키만 removeItem으로 제거합니다. 원래 clear 예제를 실행할 때에는 다른 앱의 값이 없는 전용 학습 출처를 사용하고, 이 확장을 따라 할 때에는 다른 키가 남아 있는지도 확인하세요. Web Storage의 getItem·삭제·접근 예외 규칙과 앱이 정한 만료 규칙은 서로 다른 책임입니다.
독립 연습
각 경우를 별도로 시작하고 삭제가 가능한 저장소라고 가정합니다. ① 버전 1·theme dark·expiresAt 5000을 저장한 뒤 now 4999로 읽기, ② 같은 값을 now 5000으로 읽기, ③ 유효한 JSON이지만 version 2인 값을 읽기의 상태·theme·해당 키의 남음 여부를 예상하세요. 별도 키 wb9.other는 어느 경우에도 남아야 합니다.
JSON 성공 다음의 두 검사를 적용하는 풀이
①은 fresh·dark이고 키가 남습니다. ②는 만료 경계와 같으므로 expired·light, 해당 키를 제거합니다. ③은 현재 앱의 버전 검사에서 invalid·light, 해당 키를 제거합니다. 어느 경우도 clear를 호출하지 않아 wb9.other는 유지됩니다. 만료 시간을 담은 JSON과 현재 버전의 정상 데이터라는 조건을 각각 검사해야 합니다.