JavaScript localStorage 사용하기
예제 기준 · JavaScript : 브라우저 내장 Web Storage API (별도 라이브러리 없음)
localStorage는 브라우저에 key/value 형태로 데이터를 저장하는 기능입니다. 문자열 저장과 삭제부터 객체·배열의 JSON 변환, form 복원, 목록 관리, 값의 존재 여부와 storage 이벤트까지 살펴보겠습니다. 이어서 Web Storage API에 포함된 sessionStorage와 저장 범위·유지 기간을 비교합니다.
저장소는 origin 단위로 구분됩니다. origin은 프로토콜·호스트·포트의 조합이므로 같은 호스트라도 HTTP와 HTTPS 또는 포트가 다르면 별개의 저장소입니다. URL 경로가 다르다는 이유만으로 저장소가 분리되지는 않습니다.
각 절의 코드는 해당 예제의 HTML 요소가 만들어진 뒤 독립적으로 사용합니다. 아래 두 함수는 각 예제에 함께 둡니다. show()는 pre#result에 결과를 표시하고, run()은 저장소 접근이나 JSON 처리 중 발생한 예외를 화면에 표시합니다.
function show(value) {
document.getElementById('result').textContent = String(value);
}
function run(action) {
try {
action();
} catch (error) {
show('저장소 작업 실패: ' + error.name
+ '\n브라우저의 저장소 접근 제한 또는 저장된 데이터 형식을 확인하세요.');
}
}
브라우저 설정에 따라 저장소 접근이 거부되거나 저장 한도를 넘겨 쓰기가 실패할 수 있습니다. localStorage에 접근하는 부분부터 try/catch 안에 두고, 저장이 끝난 다음 성공 메시지를 표시합니다.
기본 저장·읽기·삭제
‘저장할 값’ #value의 초기값은 Test입니다. ‘저장’ #save는 입력값을 저장하고, ‘읽기’ #read는 같은 키를 읽습니다. 키는 playground:local-storage:basic:value입니다.
const key = 'playground:local-storage:basic:value';
document.getElementById('save').addEventListener('click', function () {
run(function () {
localStorage.setItem(key, document.getElementById('value').value);
show('저장했습니다.');
});
});
document.getElementById('read').addEventListener('click', function () {
run(function () {
const value = localStorage.getItem(key);
if (value === null) {
show('null: 저장된 값이 없습니다.');
} else {
show('읽은 값: ' + JSON.stringify(value));
}
});
});
document.getElementById('remove').addEventListener('click', function () {
run(function () {
localStorage.removeItem(key);
show('이 예제의 키만 삭제했습니다.');
});
});
const clearButton = document.getElementById('clear');
clearButton.disabled = true;
document.getElementById('confirmClear').addEventListener('change', function () {
clearButton.disabled = !this.checked;
});
clearButton.addEventListener('click', function () {
if (!document.getElementById('confirmClear').checked) {
return;
}
run(function () {
localStorage.clear();
show('이 origin의 localStorage 전체를 삭제했습니다.');
document.getElementById('confirmClear').checked = false;
clearButton.disabled = true;
});
});
setItem(key, value)는 같은 키가 있으면 값을 바꾸고, getItem(key)는 저장된 문자열을 반환합니다. 키가 없으면 null입니다. ‘이 키 삭제’ #remove는 removeItem(key)로 이 항목만 지웁니다. 이미 없는 키를 지우는 것은 오류가 아닙니다.
clear()는 이 문서에서 만든 키뿐 아니라 해당 origin의 localStorage 전체를 삭제합니다. 위 코드에서는 #confirmClear를 체크해야 ‘전체 저장소 clear()’ #clear가 활성화됩니다. 삭제 후에는 체크와 버튼을 다시 비활성 상태로 돌립니다. 아래 다른 예제의 저장값도 지워질 수 있으므로, 특정 기능의 초기화에는 removeItem()을 사용합니다.
객체와 배열을 JSON으로 저장하기
localStorage의 key와 value는 문자열입니다. 객체를 그대로 전달해 저장하면 객체의 구조를 보존할 수 없습니다. JSON.stringify()로 JSON 문자열을 만들고, 읽은 문자열은 JSON.parse()로 복원합니다.
‘테스트 이름’ #nickname의 초기값은 Test User입니다. ‘객체 저장’ #save는 name과 enabled를 가진 객체를 playground:local-storage:json:profile 키에 저장합니다. ‘객체 복원’ #read는 값을 읽어 화면에 표시합니다.
const key = 'playground:local-storage:json:profile';
document.getElementById('save').addEventListener('click', function () {
run(function () {
const profile = {
name: document.getElementById('nickname').value,
enabled: true
};
const text = JSON.stringify(profile);
localStorage.setItem(key, text);
show('저장 문자열: ' + text);
});
});
document.getElementById('read').addEventListener('click', function () {
run(function () {
const text = localStorage.getItem(key);
if (text === null) {
show('먼저 저장하세요.');
return;
}
const profile = JSON.parse(text);
show('복원: ' + JSON.stringify(profile, null, 2));
});
});
초기값을 저장한 문자열은 {"name":"Test User","enabled":true}입니다. 복원하면 enabled는 문자열 "true"가 아닌 boolean true가 됩니다. 이 예제의 복원 버튼은 결과를 표시하며, 입력란의 값을 다시 채우지는 않습니다.
배열에도 같은 변환을 사용합니다. 다음 목록 예제는 ["Test", "Apple"] 같은 문자열 배열을 저장합니다. JSON.parse()가 성공해도 기대한 객체나 배열이라는 보장은 없으므로, 복원한 자료형은 별도로 확인해야 합니다. 잘못된 JSON은 예외가 발생하며 위의 run()에서 처리합니다.
form 입력값 저장하고 복원하기
이 예제는 form 전체가 아니라 ‘테스트 이름’ #nickname 하나를 문자열로 저장합니다. 키는 playground:local-storage:form:draft입니다. ‘입력값 저장’ #save로 보관하고, ‘저장값 복원’ #restore로 다시 입력란에 채웁니다.
document.getElementById('sampleForm').addEventListener('submit', function (event) {
event.preventDefault();
});
const key = 'playground:local-storage:form:draft';
function restore() {
run(function () {
const value = localStorage.getItem(key);
if (value === null) {
show('저장된 입력값이 없습니다.');
return;
}
document.getElementById('nickname').value = value;
show('저장된 입력값을 복원했습니다.');
});
}
document.getElementById('save').addEventListener('click', function () {
run(function () {
localStorage.setItem(key, document.getElementById('nickname').value);
show('입력값을 저장했습니다.');
});
});
document.getElementById('restore').addEventListener('click', restore);
document.getElementById('remove').addEventListener('click', function () {
run(function () {
localStorage.removeItem(key);
document.getElementById('nickname').value = '';
show('저장값과 입력란을 비웠습니다.');
});
});
restore();
마지막의 restore() 호출은 페이지를 불러올 때 저장값을 복원합니다. 따라서 입력값을 저장한 뒤 예제를 다시 불러와도 복원됩니다. 저장 버튼을 누르지 않은 입력은 자동으로 저장되지 않습니다.
getItem()이 null이면 입력란을 바꾸지 않고 안내만 표시합니다. 빈 문자열이 저장되어 있다면 유효한 저장값으로 보고 빈 입력란으로 복원합니다. ‘저장값 삭제’ #remove는 저장된 키와 현재 입력란을 함께 비웁니다. form의 기본 제출은 preventDefault()로 막습니다.
목록 데이터를 추가·삭제하고 다시 저장하기
‘추가할 항목’ #item의 초기값은 Test입니다. ‘항목 추가’ #add를 누르면 문자열 배열에 항목을 넣고, ul#items에 표시합니다. 각 행의 ‘항목 N 삭제’ 버튼은 해당 항목을 삭제합니다. 저장 키는 playground:local-storage:list:items입니다.
const key = 'playground:local-storage:list:items';
let items = [];
function loadItems() {
const text = localStorage.getItem(key);
items = [];
if (text !== null) {
const parsed = JSON.parse(text);
if (!Array.isArray(parsed)) {
throw new Error('목록 형식이 아닙니다.');
}
for (let i = 0; i < parsed.length; i++) {
if (typeof parsed[i] !== 'string') {
throw new Error('문자열 목록이 아닙니다.');
}
items.push(parsed[i]);
}
}
}
function render() {
const list = document.getElementById('items');
list.replaceChildren();
for (let i = 0; i < items.length; i++) {
const row = document.createElement('li');
const text = document.createElement('span');
text.textContent = items[i];
const button = document.createElement('button');
button.type = 'button';
button.textContent = '항목 ' + (i + 1) + ' 삭제';
button.addEventListener('click', function () {
run(function () {
const next = items.slice();
next.splice(i, 1);
localStorage.setItem(key, JSON.stringify(next));
items = next;
render();
});
});
row.append(text, button);
list.append(row);
}
show('저장된 항목: ' + items.length + '개');
}
document.getElementById('add').addEventListener('click', function () {
run(function () {
const value = document.getElementById('item').value.trim();
if (value === '') {
show('항목을 입력하세요.');
return;
}
loadItems();
const next = items.slice();
next.push(value);
localStorage.setItem(key, JSON.stringify(next));
items = next;
render();
});
});
run(function () {
loadItems();
render();
});
function resetExample() {
run(function () {
localStorage.removeItem(key);
items = [];
document.getElementById('item').value = 'Test';
render();
});
}
document.getElementById('resetExample').addEventListener('click', resetExample);
loadItems()는 저장된 JSON이 배열인지 확인한 다음, 일반 for문으로 모든 항목이 문자열인지 검사합니다. 저장값이 없으면 빈 배열로 시작합니다. 잘못된 JSON이나 다른 자료형이 들어 있으면 오류로 안내하고 저장값을 자동으로 덮어쓰지 않습니다.
추가할 때는 입력 앞뒤의 공백을 제거하고 빈 문자열이면 저장하지 않습니다. 저장된 목록을 다시 읽고 slice()로 복사한 next에 값을 추가한 뒤, JSON 문자열로 다시 저장합니다. 삭제할 때도 복사본에서 splice(i, 1)로 한 항목을 제거합니다. 저장이 성공한 다음 items를 교체하고 화면을 갱신하므로 쓰기 실패를 성공한 것으로 표시하지 않습니다.
‘목록 초기화’ #resetExample는 이 목록의 키만 삭제하고 입력값을 Test로 되돌립니다. 목록 항목은 textContent로 넣으므로 입력 문자열을 HTML로 실행하지 않습니다.
객체 배열을 관리할 때도 JSON 변환과 재저장 순서는 같지만 각 객체의 필드까지 검사해야 합니다. 이 예제는 문자열 배열만 허용합니다. 또한 삭제는 현재 화면의 items를 기준으로 하므로 다른 문서에서 목록을 바꾼 경우 최신 변경을 덮어쓸 수 있습니다. 여러 문서의 읽기·수정·저장을 하나의 트랜잭션처럼 보장하는 코드는 아닙니다.
빈 문자열과 없는 키 구분하기
키의 존재 여부는 값이 참인지가 아니라 getItem()의 결과가 null인지로 확인합니다. 빈 문자열은 저장된 값이므로 if (!value)만으로 존재 여부를 판정하면 구분할 수 없습니다.
‘빈 문자열 저장’ #empty, ‘키 삭제’ #remove, ‘존재 여부 확인’ #read를 비교합니다. 키는 playground:local-storage:exists:value입니다.
const key = 'playground:local-storage:exists:value';
function inspect() {
const value = localStorage.getItem(key);
show('존재: ' + (value !== null) + '\ngetItem: ' + JSON.stringify(value));
}
document.getElementById('empty').addEventListener('click', function () {
run(function () {
localStorage.setItem(key, '');
inspect();
});
});
document.getElementById('remove').addEventListener('click', function () {
run(function () {
localStorage.removeItem(key);
inspect();
});
});
document.getElementById('read').addEventListener('click', function () {
run(inspect);
});
run(inspect);
빈 문자열을 저장하면 결과는 존재: true와 getItem: ""입니다. 키를 삭제하면 존재: false와 getItem: null이 됩니다. 여기서 JSON.stringify()는 화면에 빈 문자열의 따옴표까지 표시해 null과 구분하기 위해 사용합니다.
storage 이벤트로 다른 문서의 변경 감지하기
storage 이벤트는 같은 origin에서 같은 localStorage 저장 영역을 공유하는 다른 문서의 변경을 감지할 때 사용합니다. 변경을 수행한 문서에는 그 변경에 대한 storage 이벤트가 발생하지 않습니다. 현재 화면은 저장 직후 직접 갱신하고, 다른 문서의 변경은 이벤트로 받습니다.
이벤트가 전달되는 범위는 저장소 종류에 따라 다릅니다.
- localStorage: 같은 origin의 같은 저장 영역을 공유하는 다른 document에서 받습니다. 다른 탭·창도 포함됩니다.
- sessionStorage: 같은 origin뿐 아니라 같은 top-level browsing context(최상위 탭·창)에 속해야 합니다. 같은 탭 안에서 저장 영역을 공유하는 iframe 문서 등은 받을 수 있지만, 별도 탭·창에는 전달되지 않습니다.
두 경우 모두 변경을 수행한 현재 문서에는 자신의 변경에 대한 storage 이벤트가 발생하지 않습니다. 아래 실행 예제는 localStorage를 사용합니다.
아래 예제에는 ‘두 번째 문서’가 함께 표시됩니다. 어느 쪽에서든 ‘다른 문서로 값 변경’ #save를 누르면 입력값 뒤에 현재 시각 값을 붙여 저장합니다. 변경한 쪽은 #ownStatus에 직접 변경 내용을 표시하고, 반대쪽은 pre#result에 수신 로그를 표시합니다.
다음은 두 문서에서 사용하는 저장·수신 코드입니다. 두 번째 문서를 표시하고 높이를 조절하는 코드는 제외했습니다.
const key = 'playground:local-storage:storage-event:value';
const logs = [];
document.getElementById('save').addEventListener('click', function () {
run(function () {
const value = document.getElementById('value').value + ' / ' + Date.now();
localStorage.setItem(key, value);
document.getElementById('ownStatus').textContent = '직접 변경: ' + value
+ ' (이 문서에는 storage 이벤트가 발생하지 않음)';
});
});
window.addEventListener('storage', function (event) {
if (event.storageArea !== localStorage || event.key !== key) {
return;
}
logs.push('storage | old=' + event.oldValue + ' | new=' + event.newValue);
show(logs.join('\n'));
});
show('다른 문서의 변경 대기 중 (브라우저 저장소 차단 시 사용할 수 없습니다.)');
function resetExample() {
logs.length = 0;
show('수신 로그를 초기화했습니다. 다른 문서의 변경 대기 중');
}
document.getElementById('resetExample').addEventListener('click', resetExample);
이벤트에서는 storageArea가 localStorage인지, key가 playground:local-storage:storage-event:value인지 확인합니다. 다른 예제의 키 변경은 무시합니다. ‘수신 로그 초기화’ #resetExample는 logs와 결과 표시만 비우며 저장값이나 직접 변경 표시를 지우지 않습니다.
| 정보 | 의미 |
|---|---|
| key | 변경한 키. clear()로 전체 삭제한 경우 null |
| oldValue | 이전 문자열. 새 키를 추가한 경우 null |
| newValue | 새 문자열. 키를 삭제한 경우 null |
| storageArea | 변경된 Storage 객체. localStorage 또는 sessionStorage 구분 |
clear()로 발생하는 이벤트는 key·oldValue·newValue가 모두 null입니다. 위 예제는 지정한 key만 받으므로 전체 삭제 이벤트를 무시합니다. 전체 삭제까지 화면에 반영하려면 key가 null인 경우를 별도로 처리해야 합니다.
같은 값을 다시 저장하는 등 실제 변화가 없으면 이벤트가 발생하지 않습니다. 예제에서 Date.now()를 붙이는 것은 저장값이 달라지도록 하기 위해서입니다. storage 이벤트는 변경 알림이며, 동시 쓰기의 충돌을 해결하거나 현재 문서의 갱신을 대신하지는 않습니다.
일부 브라우저의 저장소 차단·분리 정책에서는 iframe 안의 저장소 접근이나 문서 간 공유가 제한될 수 있습니다. 같은 origin이라는 조건 외에도 두 문서가 같은 저장 영역을 사용할 수 있어야 합니다.
Web Storage API란
Web Storage API는 브라우저에 key/value 문자열을 저장하는 API이며, localStorage와 sessionStorage를 포함합니다. 둘 다 Storage 인터페이스를 사용하므로 setItem(), getItem(), removeItem(), clear()의 사용법은 거의 같습니다.
두 저장소는 별개입니다. 같은 origin에서 같은 key를 사용해도 localStorage에 저장한 값을 sessionStorage에서 읽을 수는 없습니다. 차이는 데이터를 공유하는 범위와 보관 기간에 있습니다.
sessionStorage 사용하기
다음은 DOM 요소 없이 사용할 수 있는 독립 예제입니다. theme을 저장하고 읽은 뒤 해당 항목을 삭제합니다. getItem()은 문자열을 반환하며, 키가 없으면 null입니다. 저장소 접근이 제한될 수도 있으므로 접근부터 try/catch 안에 둡니다.
try {
sessionStorage.setItem('theme', 'dark');
const theme = sessionStorage.getItem('theme');
console.log(theme); // "dark"
sessionStorage.removeItem('theme');
console.log(sessionStorage.getItem('theme')); // null
} catch (error) {
console.log('세션 저장소 작업 실패: ' + error.name);
}
저장 후 removeItem()을 호출하지 않았다면 같은 탭에서 새로고침해도 값이 유지됩니다. 위 코드를 끝까지 실행하면 theme을 삭제하므로 값이 남지 않습니다.
clear()는 현재 탭·창의 해당 origin에 속한 sessionStorage 전체를 지웁니다. 특정 기능의 항목만 삭제할 때는 removeItem()을 사용합니다. 전체 초기화가 필요한 경우에만 다음 코드를 별도로 사용합니다.
try {
sessionStorage.clear();
} catch (error) {
console.log('세션 저장소 전체 삭제 실패: ' + error.name);
}
sessionStorage.clear()는 localStorage를 지우지 않습니다. 같은 탭에서 sessionStorage를 공유하는 같은 origin의 다른 문서가 만든 항목은 함께 삭제됩니다.
localStorage와 sessionStorage 차이
| 비교 항목 | localStorage | sessionStorage |
|---|---|---|
| 저장 범위 | origin 단위 | origin과 최상위 브라우징 컨텍스트(탭·창) 단위 |
| 데이터 유지 기간 | 자동 만료 기간 없이 유지. 직접 삭제나 브라우저 정책으로 제거될 수 있음 | 해당 탭·창의 페이지 세션 동안 유지. 세션 종료 시 제거 |
| 탭·창 간 공유 여부 | 같은 origin의 같은 저장 영역을 사용하는 탭·창에서 공유 | 별도 탭·창과 공유하지 않음. 같은 탭 안의 같은 origin 문서끼리는 공유 |
| 페이지 새로고침 시 유지 여부 | 유지 | 유지 |
| 브라우저 종료 이후 유지 여부 | 닫았다 다시 열어도 일반적으로 유지 | 탭·창 세션이 종료되면 제거. 다음 방문의 영구 보관 용도로 사용하지 않음 |
| 주요 사용 사례 | 테마·목록 보기 방식 등 다음 방문에도 필요한 설정 | 현재 탭의 작업 단계·임시 입력 상태 |
sessionStorage의 페이지 세션은 새로고침에도 유지됩니다. 브라우저의 세션 복원으로 데이터가 복구될 수 있으므로, 브라우저 종료를 민감정보의 확실한 삭제 수단으로 삼지는 않습니다.
opener가 있는 새 창은 처음에 opener의 sessionStorage를 복사받을 수 있습니다. 그래도 이후의 저장 영역은 별개이므로 한쪽의 변경이 다른 쪽에 동기화되지는 않습니다.
위 비교는 일반적인 저장 영역을 기준으로 합니다. 시크릿 모드, 사용자 데이터 삭제, 저장소 차단·분리 정책에 따라 유지나 접근·공유가 제한될 수 있습니다.
어떤 것을 사용해야 하는가
localStorage가 적합한 경우
- 테마·글자 크기·보기 방식 같은 사용자 UI 설정
- 현재 탭에 한정되지 않는, 임시가 아닌 작은 브라우저 측 상태
- 다음 방문에도 유지해야 하는 민감하지 않은 데이터
sessionStorage가 적합한 경우
- 현재 탭에서만 필요한 작업 상태
- 다단계 입력 화면의 진행 단계와 민감하지 않은 임시 입력값
- 탭을 닫아 세션이 끝나면 없어져도 되는 상태
두 저장소 모두 서버 저장이나 사용자 계정·기기 간 동기화를 대신하지 않습니다. 사용자가 지우거나 브라우저 정책에 따라 사라질 수 있으므로 중요한 데이터의 유일한 보관 장소로 삼지 않습니다.
Web Storage 사용 시 공통 주의사항
- 문자열 기반 저장: key와 value는 문자열입니다. 객체·배열은 JSON.stringify()로 저장하고 JSON.parse()로 복원합니다. 파싱 오류와 복원한 자료형·필드를 확인합니다.
- 동기 API: localStorage와 sessionStorage 모두 작업이 끝날 때까지 JavaScript 실행을 점유합니다. 큰 데이터를 자주 읽고 쓰거나 긴 JSON을 반복 변환하면 화면 처리가 지연될 수 있습니다.
- 용량과 접근 제한: 사용 가능한 용량은 브라우저 환경·모드·설정에 따라 다를 수 있습니다. 고정된 수치를 전제하지 않고 접근 제한과 쓰기 실패를 처리합니다. 큰 데이터 저장에는 적합하지 않습니다.
- 민감정보 저장 금지: 비밀번호·인증 토큰·민감한 개인정보를 보관하는 용도로 사용하지 않습니다. 같은 origin의 JavaScript가 접근할 수 있어 XSS로 노출될 수 있으며, sessionStorage도 이 위험을 없애지 않습니다.
- origin 단위 격리: 프로토콜·호스트·포트가 다르면 저장 영역도 다릅니다. 경로만 다르면 격리되지 않으며, sessionStorage에는 탭·창 구분도 추가됩니다.
- 삭제 범위: clear()는 선택한 저장 영역의 모든 항목을 삭제합니다. 특정 기능의 초기화에는 해당 key의 removeItem()을 사용합니다.
저장값은 신뢰할 수 있는 데이터로 간주하지 않습니다. 작은 데이터를 필요한 시점에 저장하고, 저장이 성공한 다음 화면에 성공 결과를 표시합니다.
| 목적 | 사용 방법 | 확인할 점 |
|---|---|---|
| 문자열 저장 | setItem(key, value) | 같은 키는 덮어씀, 쓰기 실패 처리 |
| 읽기·존재 확인 | getItem(key) | null과 빈 문자열 구분 |
| 한 항목 삭제 | removeItem(key) | 해당 기능의 키만 삭제 |
| 전체 삭제 | clear() | 선택한 저장 영역의 다른 키도 삭제 |
| 객체·배열 저장 | JSON.stringify() / JSON.parse() | 파싱 오류와 자료형 검사 |
| 다른 문서의 변경 감지 | storage 이벤트 | 저장소별 전달 범위 확인. 현재 문서의 직접 변경은 별도 갱신 |
