안녕하세요! 프론트엔드 개발의 든든한 무기 중 하나인 Web Storage API의 세계로 오신 것을 환영합니다. 사용자가 다크 모드를 켰는지, 로그인 상태를 유지하기로 했는지 등 소중한 설정값들을 브라우저에 찰떡같이 저장해 주는 마법 같은 기술이죠. 공식 문서가 영어라 부담스러우셨을 텐데, 제가 아주 친절한 구어체로 샅샅이 번역해 드릴게요. 실무에서 바로 써먹을 수 있는 강사만의 꿀팁도 가득 담았습니다! 자, 시작해 볼까요?
호환성 현황 (Baseline): 널리 사용 가능(Widely available)
이 기능은 아주 잘 정립되어 있으며 다양한 기기와 브라우저 버전에서 완벽하게 작동합니다. 2015년 7월부터 모든 주요 브라우저에서 사용할 수 있게 되었습니다! 마음 푹 놓고 쓰셔도 됩니다.
Web Storage API는 브라우저가 키/값 쌍(key/value pairs)을 안전하게 저장할 수 있는 메커니즘을 제공합니다.
이 문서에서는 이 기술을 실무에서 어떻게 활용하는지 단계별로 차근차근(walkthrough) 설명해 드리겠습니다.
Storage(스토리지) 객체들은 우리가 흔히 아는 자바스크립트 객체(objects)와 아주 비슷하게 생긴 단순한 '키-값 저장소'입니다. 하지만 일반 객체와의 결정적인 차이점은, 페이지를 새로고침(reload)하거나 창을 껐다 켜도 데이터가 고스란히 유지된다(stay intact)는 것입니다!
스토리지의 키(Key)와 값(Value)은 항상 문자열(strings)이어야만 합니다. (자바스크립트 일반 객체처럼 정수형 숫자를 키로 쓰더라도, 브라우저가 몰래 문자열로 싹 바꿔버린다는 점을 주의하세요). 여러분은 이 값들에 접근할 때 일반 객체처럼 점(.)을 찍어서 접근할 수도 있고, 아니면 정석대로 Storage.getItem()과 Storage.setItem() 메서드를 사용할 수도 있습니다.
아래의 세 줄은 모두 똑같이 colorSetting이라는 항목에 값을 저장하는 코드입니다.
localStorage.colorSetting = "#a4509b";
localStorage["colorSetting"] = "#a4509b";
localStorage.setItem("colorSetting", "#a4509b");
💡 중요한 참고 사항 (Note):
평범한 객체처럼.이나[]로 접근하는 것도 가능하긴 하지만, Web Storage API가 제공하는 정식 메서드들(setItem,getItem,removeItem,key,length)을 사용하는 것을 강력히 권장합니다! 일반 객체처럼 다루다 보면, 스토리지 객체가 원래 가지고 있는 내장 메서드 이름(예:length)과 우리가 저장하려는 키 이름이 겹쳐서 충돌하는 등 여러 가지 함정(pitfalls)에 빠질 수 있거든요.
Web Storage에는 다음과 같은 두 가지 메커니즘이 존재합니다:
sessionStorage: 주어진 출처(origin, 도메인)에 대해 독립적인 저장 공간을 하나 만들어줍니다. 이 데이터는 페이지 세션(page session)이 유지되는 동안에만 살아있습니다. (브라우저나 해당 탭이 열려있는 동안만 유지되며, 페이지를 새로고침하거나 복구할 때는 유지되지만, 탭을 완전히 꺼버리면 데이터도 함께 증발합니다.)localStorage: sessionStorage와 똑같은 일을 하지만, 브라우저를 완전히 껐다가 다시 켜도 데이터가 영구적으로 보존된다는 엄청난 차이가 있습니다.이 두 메커니즘은 각각 Window.sessionStorage와 Window.localStorage 속성을 통해 사용할 수 있습니다. (더 엄밀히 말하자면, 브라우저의 Window 객체가 WindowLocalStorage와 WindowSessionStorage 객체를 구현하고 있고, 이 안에 속성들이 들어있는 형태입니다.)
이 둘 중 하나를 호출하면 Storage 객체의 인스턴스가 반환되고, 바로 이 객체를 통해 데이터를 저장(set)하고, 꺼내오고(retrieve), 지울(remove) 수 있습니다. sessionStorage와 localStorage는 각각의 출처(origin)마다 서로 다른 별개의 Storage 객체를 사용합니다. 즉, 둘은 완전히 따로따로 동작하며 별개로 통제됩니다.
정리하자면, 어떤 문서에서 처음 localStorage를 호출하면 하나의 Storage 객체가 반환되고, sessionStorage를 호출하면 또 다른 별개의 Storage 객체가 반환됩니다. 둘 다 사용법은 똑같지만 저장소의 수명이 다르다는 점만 기억하세요!
localStorage를 본격적으로 사용하기 전에, 현재 브라우징 세션에서 이 기능이 제대로 지원되고 또 당장 사용할 수 있는 상태인지 먼저 검증해 보는 것이 좋습니다.
localStorage를 지원하는 브라우저들은 window 객체에 localStorage라는 이름의 속성을 가지고 있습니다. 하지만 일반적인 기능 감지(feature detection) 방식처럼 그저 window.localStorage가 존재하는지만 체크하는 것은 살짝 위험할 수 있습니다.
왜냐하면, 브라우저 설정 중에 '스토리지 API 사용 거부' 같은 기능이 켜져 있을 때 브라우저가 전역 객체 자체를 아예 숨겨버리지는 않거든요. 즉, 브라우저가 localStorage라는 기능 자체는 지원(support)하고 있지만, 현재 페이지의 스크립트가 그걸 사용(available)하지는 못하도록 막아둔 상태일 수도 있다는 뜻입니다.
예를 들어, 사용자가 브라우저의 '시크릿 모드(사생활 보호 모드)'로 접속한 경우, 일부 브라우저들은 텅 빈 localStorage 객체를 주긴 하지만 할당 용량(quota)을 0으로 만들어 버려서 사실상 데이터를 쓸 수 없게 만들어 버립니다. 반대로, 데이터를 저장하려고 할 때 합당하게 QuotaExceededError(용량 초과 에러)가 뜰 수도 있죠. 이건 저장 공간을 꽉 채워서 더 이상 못 쓴다는 뜻이지, 스토리지 자체를 '사용할 수 없는' 것은 아닙니다.
따라서 제대로 된 기능 감지 코드는 이런 복잡한 시나리오들을 모두 고려해야 합니다.
아래 코드는 localStorage가 브라우저에 존재하면서, 동시에 실제로 데이터를 썼다 지웠다 할 수 있는지(available) 완벽하게 감지해 내는 훌륭한 함수입니다.
function storageAvailable(type) {
let storage;
try {
storage = window[type]; // 'localStorage' 또는 'sessionStorage' 객체를 가져옵니다.
const x = "__storage_test__";
storage.setItem(x, x); // 임시 데이터를 써봅니다.
storage.removeItem(x); // 방금 쓴 데이터를 지워봅니다.
return true; // 이 과정이 모두 에러 없이 통과했다면 사용 가능한 것입니다!
} catch (e) {
return (
e instanceof DOMException &&
e.name === "QuotaExceededError" &&
// 만약 에러가 났는데, 그게 '용량 초과' 에러이면서, 동시에 스토리지에 이미 무언가 저장되어 있는 상태라면?
// 이건 스토리지를 쓸 수는 있는데 꽉 차서 발생한 에러이므로 사용 가능한 것으로 인정해 줍니다!
storage &&
storage.length !== 0
);
}
}
이제 이 함수를 실무에서 이렇게 활용하시면 됩니다!
if (storageAvailable("localStorage")) {
// 만세! 이제 localStorage의 놀라운 기능들을 마음껏 쓸 수 있어요!
} else {
// 아쉽게도, 여기선 localStorage를 쓸 수 없네요. (쿠키 같은 다른 방법을 찾아야겠죠?)
}
만약 sessionStorage를 테스트하고 싶다면? 간단하게 storageAvailable("sessionStorage")라고 호출하시면 됩니다.
💡 강사의 팁: 요즘 모던 브라우저들은 대부분 스토리지 기능을 지원하지만, 특히 iOS Safari의 구버전 시크릿 모드에서는
localStorage에 접근만 해도 냅다 에러를 뿜어내며 스크립트를 멈춰버리는 악명 높은 버그가 있었습니다. 그래서 위와 같이try...catch로 감싸서 안전하게 테스트하는 패턴이 필수적으로 자리 잡게 된 것이죠! 꼭 여러분의 유틸리티 파일에 복사해 두고 쓰시길 바랍니다.
웹 스토리지가 실제로 어떻게 쓰이는지 직관적으로 보여드리기 위해, 창의력을 발휘해 Web Storage Demo라는 이름의 예제를 만들어 보았습니다. 이 랜딩 페이지 링크에 들어가 보시면 배경색, 폰트 종류, 그리고 귀여운 데코레이션 이미지까지 여러분 마음대로 커스텀할 수 있는 컨트롤러가 있습니다.
[웹 스토리지 예제 화면: HEX 값으로 배경색을 정하는 텍스트 상자와 폰트 스타일, 이미지를 고르는 두 개의 드롭다운 메뉴가 보입니다.]
여러분이 옵션을 이것저것 바꿔보면, 페이지의 디자인이 즉각적으로 업데이트됩니다. 그리고 여기서 끝이 아닙니다! 여러분의 선택값들은 곧바로 localStorage에 저장됩니다. 그래서 나중에 탭을 닫고 나갔다가 며칠 뒤에 이 페이지를 다시 로드해도, 브라우저가 여러분의 취향을 그대로 기억하고 화면을 그려줄 겁니다!
또한 우리는 StorageEvent가 어떻게 발생하는지 보여주는 이벤트 출력 페이지(event output page)도 함께 준비했습니다. 여러분의 브라우저에서 새 탭을 열어 이 이벤트 페이지를 띄워두고, 기존 탭(랜딩 페이지)에서 색상이나 폰트를 바꿔보세요. 그러면 방금 바꾼 따끈따끈한 스토리지 변경 정보들이 이벤트 페이지에 실시간으로 촥촥 찍히는 마법을 보실 수 있습니다.
[이벤트 출력 페이지 화면]
참고: 위의 링크들을 통해 예제 페이지들을 직접 만져보시는 것 외에도, GitHub에 올라와 있는 전체 소스 코드를 직접 뜯어보시는 것도 강력히 추천합니다!
가장 먼저, main.js 파일의 도입부에서 우리는 스토리지 객체에 이미 데이터가 채워져 있는지(즉, 사용자가 이전에 이 페이지에 방문한 적이 있는지)를 검사합니다.
if (!localStorage.getItem("bgcolor")) {
populateStorage(); // 데이터가 없으면 초기값을 세팅해 줍니다.
} else {
setStyles(); // 데이터가 있다면 그 값을 읽어와서 화면 스타일을 업데이트합니다.
}
Storage.getItem() 메서드는 스토리지에서 특정 데이터 아이템을 꺼내올 때 사용합니다. 위 코드에서는 bgcolor라는 항목이 존재하는지 콕 찔러보고 있습니다. 만약 반환값이 null이라면(존재하지 않는다면), populateStorage() 함수를 실행해서 현재 화면에 보이는 기본 설정값들을 스토리지에 최초로 저장해 줍니다. 만약 이미 값이 들어있다면? 바로 setStyles() 함수를 실행해서 저장되어 있던 값들로 페이지의 스타일을 예쁘게 꾸며줍니다.
참고:
getItem()대신Storage.length속성을 사용해서 스토리지 객체 자체가 텅 비어있는지(0인지) 아닌지를 테스트할 수도 있습니다.
앞서 살펴봤듯이, 스토리지에 저장된 값은 Storage.getItem()을 사용해서 꺼내옵니다. 이 메서드는 꺼내오고 싶은 데이터 아이템의 키(key) 이름을 인자로 받고, 그 안에 들어있던 데이터 값을 반환합니다.
예를 들어 볼까요:
function setStyles() {
// 스토리지에서 값들을 쏙쏙 빼옵니다.
const currentColor = localStorage.getItem("bgcolor");
const currentFont = localStorage.getItem("font");
const currentImage = localStorage.getItem("image");
// 페이지를 새로고침했을 때, 폼(form)의 입력창들도 저장된 값과 똑같이 맞춰줍니다.
document.getElementById("bgcolor").value = currentColor;
document.getElementById("font").value = currentFont;
document.getElementById("image").value = currentImage;
// 마지막으로, 저장된 값들을 사용해 실제 DOM 요소들의 스타일과 이미지를 업데이트합니다!
htmlElem.style.backgroundColor = `#${currentColor}`;
pElem.style.fontFamily = currentFont;
imgElem.setAttribute("src", currentImage);
}
위 코드의 첫 세 줄은 로컬 스토리지에서 데이터들을 잡아채 옵니다.
그다음에는 폼(form) 요소들의 값을 이 스토리지 값들로 덮어씌워 줍니다. 이렇게 해야 사용자가 페이지를 새로고침해도 드롭다운 메뉴나 인풋창이 엉뚱한 기본값을 가리키지 않고, 저장된 상태(sync)를 그대로 유지하게 됩니다.
마지막으로 쨘! 페이지의 배경색과 글꼴, 데코레이션 이미지를 저장된 값에 맞춰서 렌더링해 줍니다.
Storage.setItem()은 완전히 새로운 데이터를 스토리지에 밀어 넣거나, 만약 이미 똑같은 키(key) 이름이 존재한다면 기존 값을 새로운 값으로 시크하게 덮어써버리는 역할을 합니다. 이 녀석은 두 개의 인자를 받습니다. 첫 번째는 우리가 만들거나 수정할 데이터의 키(key) 이름이고, 두 번째는 거기에 담을 실제 값(value)입니다.
function populateStorage() {
// 사용자가 컨트롤러(form)에 입력한 현재 값들을 가져와서 스토리지에 꾹꾹 눌러 담습니다.
localStorage.setItem("bgcolor", document.getElementById("bgcolor").value);
localStorage.setItem("font", document.getElementById("font").value);
localStorage.setItem("image", document.getElementById("image").value);
// 방금 저장한 따끈따끈한 값들로 화면을 갱신합니다.
setStyles();
}
populateStorage() 함수는 로컬 스토리지에 배경색, 폰트, 이미지 경로라는 세 가지 항목을 세팅합니다. 그리고 곧바로 setStyles() 함수를 호출해서 방금 저장한 정보대로 페이지 디자인을 휙 바꿔버리죠.
우리는 또한 각각의 폼(form) 요소들(색상 선택기, 드롭다운 등)에 onchange 이벤트 핸들러를 달아두었습니다. 이렇게 하면 사용자가 컨트롤러의 값을 깔짝깔짝 바꿀 때마다 데이터와 스타일이 즉각적으로 갱신됩니다!
// 값 변경이 감지되면 지체 없이 스토리지 갱신 함수를 때려버립니다!
bgcolorForm.onchange = populateStorage;
fontForm.onchange = populateStorage;
imageForm.onchange = populateStorage;
🚨 주의! 객체나 배열을 저장하고 싶다면?
Storage는 태생적으로 오직 문자열(strings)만 저장하고 꺼낼 수 있습니다. 만약 여러분이 복잡한 일반 객체(Object)나 배열(Array) 같은 다른 타입의 데이터를 저장하고 싶다면, 반드시 그것들을 먼저 문자열로 변신시켜야 합니다! 이럴 때 우리의 구원자, JSON.stringify()를 사용하시면 됩니다.
const person = { name: "Alex" }; // 자바스크립트 객체입니다.
// 실수하는 케이스: 객체를 문자열로 바꾸지 않고 그냥 욱여넣음!
localStorage.setItem("user", person);
console.log(localStorage.getItem("user")); // "[object Object]"가 나옵니다. 완전 쓸모없는 쓰레기 데이터죠!
// 정답 케이스: JSON 문자열로 예쁘게 포장해서 넣음!
localStorage.setItem("user", JSON.stringify(person));
// 꺼낼 때는 JSON.parse()로 다시 원래의 예쁜 객체 형태로 되돌려줍니다.
console.log(JSON.parse(localStorage.getItem("user"))); // { name: "Alex" } 쨘!
하지만 유의할 점은, 함수나 무한 순환 구조 같은 임의의 복잡한 데이터 타입까지 모두 완벽하게 저장해 주는 마법 같은 방법은 없다는 것입니다. 게다가 스토리지에서 꺼내온 객체는 원본 객체의 깊은 복사본(deep copy)이기 때문에, 꺼내온 객체를 지지고 볶고 수정해 봤자 원본 객체에는 아무런 영향도 미치지 않습니다.
storage 이벤트는 같은 저장 공간(스토리지)을 공유하고 있는 "다른" 문서(탭이나 창)에서 Storage 객체에 어떤 변경(추가, 수정, 삭제)이 일어날 때마다 빠짐없이 불이 붙듯 발생(fire)합니다.
여기서 굉장히 헷갈리시면 안 되는 점이 있습니다! 이 이벤트는 데이터를 직접 수정하고 있는 바로 그 현재 페이지에서는 절대 발생하지 않습니다. 오직 같은 출처(origin)를 공유하며 띄워져 있는 "나머지 다른 탭(페이지)들"에게 "야, 방금 누가 스토리지 건드렸어! 너희도 화면 동기화해!"라고 알려주기 위한 용도입니다. 당연히 출처가 다른(예: 네이버와 다음) 페이지들끼리는 같은 스토리지 객체에 접근할 수 없으니 이 이벤트도 공유되지 않습니다.
localStorage의 경우, 같은 출처의 모든 탭들 사이에서 이벤트가 핑퐁 치듯 공유됩니다.sessionStorage의 경우, 스토리지가 딱 그 탭 안에서만 유효하기 때문에, 같은 탭 안에 들어있는 여러 개의 iframe들(동일 출처일 때) 사이에서만 제한적으로 이벤트가 공유됩니다.앞서 보여드렸던 이벤트 페이지(events.js)를 구동하는 아주 심플한 자바스크립트는 이렇습니다:
// 누군가 스토리지를 건드리면 이 이벤트 리스너가 반응합니다.
window.addEventListener("storage", (e) => {
// 이벤트 객체(e)가 들고 온 유용한 정보들을 화면에 예쁘게 찍어줍니다.
document.querySelector(".my-key").textContent = e.key; // 방금 변경된 항목의 키 이름!
document.querySelector(".my-old").textContent = e.oldValue; // 바뀌기 전의 과거 값!
document.querySelector(".my-new").textContent = e.newValue; // 바뀌고 난 후의 새로운 값!
document.querySelector(".my-url").textContent = e.url; // 스토리지를 건드린 범인(페이지)의 URL 주소!
// 변경이 일어난 스토리지 공간 전체의 현재 모습도 문자열로 바꿔서 싹 다 보여줍니다.
document.querySelector(".my-storage").textContent = JSON.stringify(
e.storageArea,
);
});
여기서는 현재 출처와 연결된 Storage 객체가 누군가에 의해 변경되었을 때 발동하도록 window 객체에 이벤트 리스너를 달았습니다. 위의 코드에서 볼 수 있듯이, 이 이벤트에 딸려오는 이벤트 객체(e) 안에는 정말 쏠쏠하고 유용한 정보들이 한가득 들어있답니다! (어떤 키가 바뀌었는지, 예전 값이 무엇이었는지, 새 값이 무엇인지 등)
💡 강사의 팁: 요즘 같은 시대에는 사용자들이 탭을 여러 개 열어놓고 사이트를 이용하는 경우가 잦습니다. 예를 들어 A 탭에서 쇼핑몰 장바구니에 물건을 담았을 때, B 탭에서도 장바구니 아이콘의 숫자가 실시간으로 '1' 올라가게 만들고 싶다면? 굳이 무거운 서버 통신을 할 필요 없이, 바로 이
storage이벤트 리스너 하나면 탭 간의 완벽한 데이터 동기화(Cross-tab communication)를 아주 가볍게 구현할 수 있답니다! 실무에서 정말 많이 쓰이는 고급 테크닉이죠.
웹 스토리지는 데이터를 삭제하기 위한 아주 심플하고 쿨한 메서드 두 가지도 함께 제공합니다. 이번 데모에서는 쓰지 않았지만, 여러분의 프로젝트에 추가하는 건 식은 죽 먹기죠:
Storage.removeItem(): 이 녀석은 지우고 싶은 데이터 아이템의 키(key) 이름, 딱 하나만 인자로 받습니다. 그리고 스토리지에서 그 키와 묶여있던 데이터를 깔끔하게 제거해 버립니다.Storage.clear(): 이 녀석은 인자를 아예 받지 않습니다. 무자비하게도 해당 출처(origin)에 속한 스토리지 전체를 텅텅 비워버립니다. 싹쓸이 지우개죠! (로그아웃할 때 주로 씁니다.)수고하셨습니다! Web Storage API를 활용해 사용자 맞춤 설정도 저장해 보고, 탭 간 통신까지 할 수 있게 되었네요. 앞으로 여러분의 웹사이트를 더 똑똑하고 편리하게 만드는 데 큰 도움이 될 겁니다. 코딩하시다 막히는 부분이 있으면 언제든 편하게 질문해 주세요! 화이팅! 🚀