로컬 스토리지와 세션 스토리지
문자열 키·값을 저장하는 Web Storage API를 익히고 localStorage·sessionStorage·쿠키의 수명과 전송 차이를 비교합니다.
앞 글 Fetch API와 AJAX에서는 서버와 비동기로 통신하는 방법을 다뤘습니다. 서버에서 데이터를 받아 화면에 보여 주는 일도 중요하지만, 서버를 거치지 않고 브라우저 안에 데이터를 저장해 둬야 할 때도 있습니다. 다크 모드 같은 화면 설정, 장바구니 임시 보관, 작성 중인 폼 내용처럼 다음에 다시 열었을 때 이어서 쓰고 싶은 값들이 그렇습니다.
예전에는 이런 값을 쿠키(cookie)에 저장했습니다. 하지만 쿠키는 용량이 약 4KB로 작고, 도메인과 경로, 보안 설정에 맞는 HTTP 요청마다 서버로 자동 전송되어 쓸데없는 트래픽을 만들며, 다루는 API도 불편합니다. HTML 표준의 웹 스토리지(Web Storage) API는 더 큰 용량과 단순한 API로 이 한계를 보완합니다. 웹 스토리지에는 로컬 스토리지(local storage)와 세션 스토리지(session storage) 두 가지가 있으며, 이 글에서는 두 저장소의 특징과 사용법, 쓰는 상황을 차례로 정리합니다.
저장 위치 고르기
DECISION TREE · STORAGE SECURITY & LIFETIME
브라우저 저장소 선택 계약
민감한 원문과 권한 판단은 서버에 둔다. 브라우저가 저장할 비민감 값은 요청 전송·탭 수명·조회 방식에 따라 저장소를 고른다.
각 예는 해당 저장소로 종결하고 아니오는 다음 질문으로 이어집니다. Cookie의 연결값과 서버의 권한 검증, JavaScript가 읽는 비민감 저장 값을 구별합니다. Cookie 속성 하나만으로 모든 요청 보안이 완성되지는 않습니다.
Same Origin은 XSS로부터 값을 숨겨 주지 않는다. sessionStorage/localStorage/IndexedDB는 JavaScript가 읽을 수 있으므로 민감 원문을 저장하지 않고, 보안·수명·조회 방식 순으로 선택한다. localStorage는 명시적 만료가 없지만 사용자 삭제·브라우저 정책으로 지워질 수 있다.
위 그림은 데이터를 어디에 저장할지 고르는 질문을 차례로 보여 줍니다. 각 질문에서 “예”가 나오면 그 저장소로 정하고, “아니오”면 다음 질문으로 넘어갑니다. 첫 질문은 비밀번호 같은 민감한 원래 값이나 사용자의 권한 상태인지입니다. 그렇다면 브라우저가 아니라 서버에 두고 서버가 검증해야 합니다. 브라우저에는 세션 ID처럼 그 자체로는 의미가 없는 연결 값만 따로 둡니다.
두 번째 질문은 서버로 자동 전송되어야 하거나, 자바스크립트가 읽지 못하게 하는 HttpOnly가 필요한지입니다. 그렇다면 쿠키를 쓰고, 서버는 요청마다 그 값을 검증합니다. 그다음 탭을 닫으면 사라져도 되는 값이라면 sessionStorage를, 큰 구조의 데이터를 비동기로 검색해야 한다면 IndexedDB를 씁니다. 어느 쪽도 아닌 작고 민감하지 않은 문자열은 localStorage에 둡니다.
그림 아래의 설명처럼 동일 출처 정책은 다른 사이트로부터 값을 지켜 줄 뿐, 내 페이지에서 실행되는 악성 스크립트(XSS)로부터 값을 숨겨 주지는 않습니다. 세 저장소는 모두 자바스크립트로 읽을 수 있으므로 민감한 원래 값은 저장하지 말고, 보안, 수명, 조회 방식의 순서로 고르세요. 또 localStorage는 만료 시간이 없지만, 사용자가 사이트 데이터를 지우거나 브라우저의 정책에 따라 지워질 수 있습니다.
웹 스토리지의 기본
웹 스토리지는 브라우저에 키와 값의 쌍을 저장하는 웹 API입니다. 동일 출처 정책을 따르므로 출처(프로토콜, 호스트, 포트)마다 저장 공간이 따로 있고, example.com에서 저장한 데이터는 another.com에서 읽을 수 없습니다.
두 저장소는 window.localStorage와 window.sessionStorage로 접근하며 메서드도 같습니다. 차이는 데이터가 얼마나 오래 유지되는지, 즉 지속성(persistence)입니다. 또 두 저장소 모두 키와 값을 문자열로만 저장한다는 점을 기억해 두세요.
로컬 스토리지
로컬 스토리지는 정해진 만료 시간이 없는 저장소로, 브라우저를 닫았다 다시 열어도 데이터가 남아 있습니다. 같은 출처라면 어느 탭이나 창에서든 같은 데이터를 읽고 쓸 수 있습니다. 용량은 브라우저마다 다르지만 출처마다 보통 5MB 안팎이며, 그 이상 저장하려고 하면 setItem()이 QuotaExceededError를 던집니다. 다만 사용자가 사이트 데이터를 지우거나 브라우저가 저장 공간을 정리하면 사라질 수 있으므로, 영원히 보존된다고 생각하면 안 됩니다.
데이터 저장과 조회
localStorage.setItem(key, 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)는 저장된 문자열을 돌려주고, 그 키가 없으면 null을 돌려줍니다. JSON으로 저장한 객체나 배열은 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)는 키 하나를 지웁니다. 다음 코드는 앞에서 저장한 theme만 지운 뒤, 남은 username과 지운 theme을 다시 읽어 비교합니다. 없는 키를 지우려 해도 오류는 나지 않고 아무 일도 일어나지 않습니다.
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')); // 결과: null키 순회
localStorage.length로 저장된 항목의 개수를, localStorage.key(index)로 그 위치의 키를 얻을 수 있습니다. 인덱스는 0부터 length - 1까지이므로, 다음 코드처럼 일반 for 문으로 키를 하나씩 꺼낸 뒤 getItem()으로 값을 읽습니다.
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)}`);
// }
// }키의 순서는 브라우저가 정하므로 저장한 순서와 같다고 기대하면 안 됩니다. for...in으로도 순회할 수 있지만 length나 getItem 같은 메서드 이름까지 나올 수 있어 권하지 않습니다. 모든 키가 필요하다면 Object.keys(localStorage)를 쓰는 방법도 있습니다.
로컬 스토리지를 쓰는 곳
다크 모드나 라이트 모드 같은 테마, 언어, 알림 설정처럼 다음에 방문해도 유지되어야 하는 사용자 설정에 알맞습니다. 브라우저를 닫아도 남아야 하는 장바구니 내용이나, 서버 요청을 줄이기 위해 자주 쓰는 응답을 잠시 저장해 두는 캐시에도 씁니다.
로그인 화면에서 마지막으로 고른 표시 방식 같은 민감하지 않은 설정은 저장해도 되지만, 인증 토큰은 다르게 봐야 합니다. 같은 출처의 자바스크립트라면 누구나 로컬 스토리지를 읽을 수 있어, XSS 공격을 받으면 토큰이 그대로 새어 나갑니다. 인증을 설계할 때는 이런 노출 위험과 함께 HttpOnly 쿠키 같은 대안을 검토해야 합니다.
다른 탭의 변경 알아채기
같은 출처의 다른 탭에서 로컬 스토리지를 바꾸면, 나머지 탭의 window에 storage 이벤트가 발생합니다. 값을 바꾼 탭 자신에서는 이 이벤트가 발생하지 않습니다.
window.addEventListener('storage', (event) => {
if (event.key === 'theme') {
console.log('다른 탭에서 테마가 바뀌었습니다:', event.oldValue, '→', event.newValue);
}
});event.key는 바뀐 키, event.oldValue와 event.newValue는 바뀌기 전과 후의 문자열이며, 키를 지웠다면 newValue는 null입니다. 이 이벤트를 받아 화면의 테마를 다시 적용하면 여러 탭의 설정을 맞출 수 있습니다.
세션 스토리지
세션 스토리지는 로컬 스토리지와 같은 메서드를 쓰지만, 데이터가 현재 탭의 세션 동안만 유지됩니다. 같은 탭에서 새로고침하거나 다른 페이지로 갔다 돌아와도 남아 있지만, 그 탭이나 창을 닫으면 지워집니다. 또 같은 출처라도 다른 탭은 각자의 세션 스토리지를 가지므로 서로의 값을 읽을 수 없습니다. 만료 시간은 따로 정할 수 없고, 용량은 로컬 스토리지와 비슷합니다.
localStorage 대신 sessionStorage를 쓰면 됩니다.
// 데이터 저장
sessionStorage.setItem('tempMessage', '이 메시지는 현재 세션 동안만 유지됩니다.');
sessionStorage.setItem('lastVisitedPage', '/products/detail/123');
// 데이터 조회
const message = sessionStorage.getItem('tempMessage');
console.log("세션 저장 메시지:", message);
// 데이터 삭제
sessionStorage.removeItem('lastVisitedPage');
sessionStorage.clear(); // 현재 세션의 모든 데이터 삭제세션 스토리지는 한 탭 안에서만 잠시 필요한 데이터에 알맞습니다. 페이지를 옮겼다 돌아와도 남아 있어야 하는 폼의 입력값, 한 번만 보여 줄 알림 메시지, 여러 단계로 나뉜 회원 가입 폼에서 다음 단계로 넘기기 전까지 각 단계의 입력을 보관하는 일 등에 씁니다.
쿠키와 웹 스토리지 비교
| 특징 | 쿠키 | 로컬 스토리지 | 세션 스토리지 |
|---|---|---|---|
| 지속성 | 만료일 지정 가능 (정하지 않으면 세션 동안) | 만료 없음 (브라우저를 닫아도 유지) | 탭을 닫으면 삭제 |
| 용량 | 약 4KB | 출처마다 보통 5MB 안팎 | 출처마다 보통 5MB 안팎 |
| 서버 전송 | 조건에 맞는 HTTP 요청에 자동 전송 | 자동 전송 안 됨 | 자동 전송 안 됨 |
| 접근 범위 | 쿠키의 Domain·Path 범위에 맞는 모든 탭, 서버 (HttpOnly면 서버만) | 같은 출처의 모든 탭 | 같은 출처의 같은 탭 |
| API | 불편함 (document.cookie 문자열을 직접 해석) | 간단함 (setItem, getItem 등) | 간단함 (setItem, getItem 등) |
| 주요 용도 | 세션 ID 같은 인증 연결 값, 추적 | 사용자 설정, 캐시, 민감하지 않은 오프라인 데이터 | 폼 입력, 임시 상태, 일회성 메시지 |
문자열로 된 설정을 저장할 때는 웹 스토리지가 편리하고, 서버로 자동 전송해야 하거나 HttpOnly가 필요한 값에는 쿠키가 알맞습니다. 쿠키는 매 요청에 함께 전송되므로, 서버가 읽고 써야 하는 세션 ID 같은 작은 값에만 제한적으로 쓰는 것이 좋습니다. 용량, 수명, 누가 읽을 수 있어야 하는지를 기준으로 고르면 됩니다. 쿠키가 Domain, Path 같은 속성에 따라 어떤 요청에 실리고 HttpOnly가 무엇을 막는지는 네트워크 교재의 쿠키와 세션에서 자세히 다룹니다.
더 깊이: 용량이 차면, 그리고 동기 API의 비용
MDN의 저장 한도 설명에 따르면 브라우저는 출처마다 로컬 스토리지와 세션 스토리지에 각각 5MiB까지 저장할 수 있습니다. 이 한도를 넘는 값을 넣으면 setItem()이 QuotaExceededError 예외를 던집니다. 그래서 크기를 미리 알 수 없는 값을 저장하는 코드는 try...catch로 감싸, 실패했을 때 오래된 키를 지우거나 저장하지 않고 계속 동작하도록 만들어 둡니다.
웹 스토리지의 메서드는 Promise를 돌려주지 않는 동기 API입니다. getItem()과 setItem()은 값을 다 읽거나 쓸 때까지 메인 스레드를 붙잡으므로, web.dev의 웹 저장소 안내는 로컬 스토리지가 메인 스레드를 막는다는 이유로 피하라고 권합니다. 테마 이름처럼 작은 값을 가끔 읽는 정도라면 문제가 되지 않지만, 큰 JSON을 자주 읽고 쓰면 그만큼 화면의 반응이 늦어질 수 있습니다.
같은 안내는 큰 데이터나 구조가 있는 데이터에는 비동기로 동작하는 IndexedDB를, 앱을 불러오는 데 필요한 네트워크 자원에는 Cache API를 쓰라고 권합니다. 그래서 웹 스토리지는 이 글에서처럼 문자열로 된 작은 설정값을 위한 저장소로 생각하는 편이 맞습니다.
만료와 버전을 함께 저장하기
웹 스토리지는 만료 시간을 관리해 주지 않습니다. 그래서 저장하는 값 안에 데이터 형식의 버전과 만료 시각을 함께 넣어 두고, 읽을 때 앱이 직접 검사하면 오래되었거나 형식이 바뀐 데이터가 화면을 망치는 일을 줄일 수 있습니다. 이렇게 데이터를 감싸 저장하는 객체를 흔히 봉투(envelope)라고 부릅니다.
저장 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 결정은 별도 관찰이다 |
위 그림은 저장해 둔 테마를 복원하는 순서입니다. 먼저 getItem()으로 키 하나를 읽고 값이 있는지 확인합니다. 값이 있으면 JSON.parse()로 문법을 검사하고, 버전이 1인지와 theme이 올바른 형태인지 확인한 뒤, 마지막으로 현재 시각이 만료 시각 expiresAt보다 이른지 봅니다. 모두 통과하면 fresh 상태로 저장된 테마를 복원합니다.
중간에 문법, 버전과 형태, 만료 가운데 하나라도 실패하면 그 키를 지우려고 시도하고, 지웠는지(removed)를 확인한 뒤 기본 테마 light를 씁니다. 저장소에 접근하지 못했거나 값이 아예 없을 때도 기본 테마를 쓰되, 그 원인은 상태 값으로 구별해 둡니다. 그림 아래의 설명처럼 JSON.parse()가 성공했다고 구조와 버전, 만료까지 검증된 것은 아니며, 웹 스토리지가 expiresAt을 보고 값을 지워 주지도 않으므로 앱이 읽을 때 판단해야 합니다.
다음 코드는 민감하지 않은 화면 설정 하나만 다룹니다. 저장 키는 wb9.theme, 데이터는 light 또는 dark, 버전은 1입니다. expiresAt과 현재 시각은 밀리초 정수로 비교하며, 현재 시각이 만료 시각과 같으면 만료로 봅니다. 저장하고 지우는 예제이므로 쓰기와 삭제가 허용된 학습용 출처에서 실행하세요.
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예제의 1000, 2000 같은 작은 시각은 만료 시점 앞뒤를 재현하기 위한 가상의 시각입니다. 실제로 복원할 때는 restoreTheme(Date.now())처럼 같은 단위의 현재 시각을 넘깁니다. restoreTheme()의 try는 브라우저 설정 때문에 window.localStorage에 접근하는 것 자체가 예외를 던지는 경우를 처리하고, readTheme() 안의 try는 getItem()과 removeItem()의 예외를 처리합니다. 저장소에 접근하거나 값을 지울 수 없는 환경도 있으므로, unavailable 상태나 removed: false를 정상 복원과 구별해 둡니다.
이 코드는 clear() 대신 removeItem()으로 자기 설정 키만 지웁니다. 앞에서 본 것처럼 clear()는 이 출처의 저장소 전체를 지우므로, 다른 기능이 쓰는 키까지 사라집니다. 저장하는 쪽에서도 setItem()이 용량 초과나 접근 제한으로 실패할 수 있으니 따로 처리해야 합니다.
정리
- 웹 스토리지는 출처마다 따로 있는 브라우저의 키-값 저장소이며, 키와 값을 문자열로만 저장하므로 객체는
JSON.stringify()와JSON.parse()로 바꿔 씁니다. localStorage는 만료 없이 같은 출처의 모든 탭이 함께 쓰고,sessionStorage는 그 탭을 닫으면 사라지며 탭마다 따로 있습니다.- 다른 탭이
localStorage를 바꾸면 나머지 탭에storage이벤트가 발생합니다. - 쿠키는 HTTP 요청에 자동 전송되고
HttpOnly로 자바스크립트에서 숨길 수 있어, 서버가 다뤄야 하는 작은 연결 값에 씁니다. - 웹 스토리지는 자바스크립트로 누구나 읽을 수 있으므로 인증 토큰 같은 민감한 값을 저장하지 않습니다.
- 만료와 버전은 저장소가 관리하지 않으므로, 값 안에 함께 넣고 읽을 때 문법, 형태, 만료를 차례로 검사합니다.
연습 문제
1. 만료 시점과 버전
각 경우를 따로 시작하고, 값을 지울 수 있는 저장소라고 가정합니다. ① 버전 1, theme dark, expiresAt 5000을 저장한 뒤 now 4999로 읽기, ② 같은 값을 now 5000으로 읽기, ③ JSON 문법은 맞지만 version이 2인 값을 읽기에서 각각의 상태, 테마, 그 키가 남는지를 예상해 보세요. 별도의 키 wb9.other는 어느 경우에도 남아야 합니다.
해설 보기
①은 fresh, dark이고 키가 남습니다. ②는 현재 시각이 만료 시각과 같아 expired, light가 되고 그 키를 지웁니다. ③은 버전 검사에서 invalid, light가 되고 그 키를 지웁니다.
세 경우 모두 clear()를 호출하지 않으므로 wb9.other는 그대로 남습니다. 만료 시각이 들어 있는 올바른 JSON이라는 것과, 현재 앱이 쓰는 버전의 데이터라는 것은 따로 검사해야 하는 조건입니다.
2. 문자열로 바뀌는 값
다음 코드의 두 출력은 무엇일까요?
localStorage.setItem('count', 5);
console.log(localStorage.getItem('count') + 1);
localStorage.setItem('hidden', false);
if (localStorage.getItem('hidden')) {
console.log('숨김 상태입니다');
} else {
console.log('보이는 상태입니다');
}해설 보기
51과 숨김 상태입니다가 출력됩니다. 숫자 5는 문자열 "5"로 저장되므로 getItem()이 돌려준 "5"에 1을 더하면 문자열 연결이 일어나 "51"이 됩니다.
불리언 false도 문자열 "false"로 저장되는데, 비어 있지 않은 문자열은 참으로 평가되므로 if 조건이 참이 됩니다. 숫자는 Number()로, 불리언은 === "true"로 비교하거나 처음부터 JSON.stringify()와 JSON.parse()로 저장하고 읽어야 원래 타입을 되살릴 수 있습니다.
3. storage 이벤트는 어디서 발생할까
같은 페이지를 탭 A와 탭 B에 열고, 두 탭 모두 앞 절의 storage 이벤트 리스너를 등록했습니다. 탭 A에서 localStorage.setItem('theme', 'dark')를 실행하면 어느 탭에 로그가 찍힐까요? 같은 일을 sessionStorage로 하면 어떻게 될까요?
해설 보기
탭 B에만 로그가 찍힙니다. storage 이벤트는 값을 바꾼 문서가 아니라, 같은 저장소를 함께 쓰는 다른 문서에 발생합니다. 탭 A는 자신이 바꾼 것을 이미 알고 있으므로 이벤트를 받지 않습니다.
sessionStorage는 탭마다 따로 있으므로 탭 A의 변경이 탭 B의 저장소에 영향을 주지 않고, 탭 B에도 이벤트가 발생하지 않습니다. 여러 탭의 설정을 맞추고 싶다면 함께 쓰는 localStorage의 이벤트를 써야 합니다.
getItem()과 삭제, 접근 예외의 규칙은 HTML 표준의 Storage 인터페이스에서, 저장소별 특징은 MDN의 Web Storage API 문서에서 확인할 수 있습니다. 만료를 언제로 볼지는 표준이 아니라 앱이 정하는 규칙입니다.
다음 글 History API와 라우팅에서는 페이지를 새로 불러오지 않고 주소와 화면을 함께 바꾸는 History API와, 이를 이용한 클라이언트 라우팅을 살펴봅니다.