로컬 스토리지
고친 사람 github-actions[bot]
로컬 스토리지는 웹 페이지가 방문자의 브라우저에 작은 값을 적어 두게 해 줍니다. 탭을 닫거나 컴퓨터를 껐다 켜도 그 값은 남습니다. 다음에 같은 사이트를 열면 페이지가 그 값을 다시 읽어 씁니다. 서버 분야에서는 같은 이름으로 서버에 직접 꽂힌 디스크를 부르기도 합니다.
쉽고 빠른 이해
로컬 스토리지는 페이지가 이름 하나에 문자열 하나를 짝지어 브라우저에 맡겨 두는 곳입니다. 방문자가 고른 어두운 화면 설정을 적어 두면 다음 방문 때도 어두운 화면으로 열립니다.
이게 없으면 페이지는 새로고침할 때마다 다 잊습니다. 설정 하나를 기억하려고 서버에 저장하거나, 요청마다 서버로 실려 가는 쿠키에 넣어야 합니다.
- 페이지가 이름과 값을 넘겨 적어 둡니다
- 브라우저가 그 값을 사이트별로 갈라 남겨 둡니다
- 다음에 같은 사이트가 열리면 이름을 대고 값을 꺼냅니다
대가도 있습니다. 문자열만 담으므로 객체는 문자열로 바꿔 넣어야 합니다. 담는 양이 작습니다. 읽고 쓰는 동안 화면이 기다립니다. 같은 사이트에서 실행되는 스크립트는 모두 읽을 수 있어서, 로그인 토큰을 두면 끼어든 악성 스크립트도 그 토큰을 가져갑니다.
상세
로컬 스토리지는 브라우저가 JavaScript 코드에 내주는 저장소입니다. 코드에서는 localStorage 라는 이름의 객체로 보입니다. 페이지는 이 객체의 메서드를 불러 값을 넣고 꺼냅니다.
이렇게 한 프로그램이 다른 프로그램의 기능을 불러 쓰도록 내준 창구를 API(Application Programming Interface, 응용 프로그램 인터페이스)라고 합니다. 로컬 스토리지는 브라우저가 페이지에 내주는 API 가운데 하나입니다.
이런 저장소가 필요한 것은 페이지의 변수가 새로고침 한 번에 사라지기 때문입니다. 방문자가 고른 설정 하나를 다음 방문까지 기억하려면 서버에 저장하거나 브라우저 쪽에 적어 둘 곳이 있어야 합니다. 로컬 스토리지는 서버를 거치지 않고 브라우저 쪽에 적어 두는 길입니다.
이 절은 값이 담기는 모양과 갈려 놓이는 범위에서 시작합니다. 이어서 메서드와 문자열 변환을 코드로 보입니다. 다른 브라우저 저장소와 견준 뒤, 비밀값을 두면 생기는 일과 쓰는 곳으로 끝냅니다.
키와 값의 짝
로컬 스토리지는 키 하나에 값 하나를 짝지어 담습니다. 키는 값을 찾을 때 대는 이름입니다. theme 이라는 키에 dark 라는 값을 넣어 두면, 나중에 theme 을 대고 dark 를 돌려받습니다.
이런 모양의 저장소를 키-값 저장소라고 부릅니다. 테이블도 열도 없고 조건을 거는 질의도 없습니다. 키를 알면 꺼내고, 모르면 못 꺼냅니다.
키와 값은 모두 문자열입니다. 숫자나 객체를 넣으면 어떻게 되는지는 아래 「문자열만 담는 저장소」 소절에서 다룹니다.
출처마다 갈리는 저장 공간
브라우저는 로컬 스토리지를 출처마다 따로 둡니다. 출처는 주소 앞부분의 프로토콜과 호스트와 포트를 한 벌로 묶은 것입니다. 개요와 「쉽고 빠른 이해」에서 로컬 스토리지가 사이트별로 갈린다고 한 그 범위가 이 출처입니다.
출처는 흔히 한 사이트로 여기는 범위보다 좁습니다. https://shop.example.com 과 https://blog.example.com 은 둘 다 example.com 아래에 있지만 호스트가 달라서 다른 출처입니다.
한 출처의 페이지는 다른 출처의 로컬 스토리지를 읽지도 쓰지도 못합니다. 출처끼리 데이터를 이렇게 갈라 두는 규칙이 동일 출처 정책입니다. 쇼핑몰 페이지가 블로그 페이지의 설정을 엿보지 못하는 것도 이 규칙 때문입니다.
같은 출처라면 탭이 몇 개든 저장 공간 하나를 함께 씁니다. 한 탭에서 적은 값을 다른 탭에서 곧바로 읽습니다. 아래 그림은 탭 셋이 두 출처의 저장 공간에 붙은 모습입니다. 쇼핑몰은 theme 키에 화면 설정을, 블로그는 lang 키에 언어 설정을 적어 두었습니다.
flowchart TD
T1["탭 1 · 쇼핑몰"] --> SA
T2["탭 2 · 쇼핑몰"] --> SA
T3["탭 3 · 블로그"] --> SB
subgraph A["출처 https://shop.example.com"]
SA["로컬 스토리지 · theme → dark"]
end
subgraph B["출처 https://blog.example.com"]
SB["로컬 스토리지 · lang → ko"]
end
탭 1 과 탭 2 는 같은 값을 봅니다. 탭 3 은 쇼핑몰의 theme 을 볼 길이 없습니다.
주소가 같아도 http:// 와 https:// 는 프로토콜이 달라 다른 출처입니다. 사이트를 https:// 로 옮기면 옛 주소에서 적어 둔 값이 새 주소에서는 안 보입니다.
부르는 메서드
로컬 스토리지를 다루는 방법은 몇 가지뿐입니다. 아래 표가 그 전부입니다.
| 부르는 것 | 하는 일 |
|---|---|
setItem(키, 값) |
키에 값을 적는다. 이미 있으면 덮어쓴다 |
getItem(키) |
키의 값을 돌려준다. 없으면 null |
removeItem(키) |
키와 값을 지운다 |
clear() |
이 출처의 키를 모두 지운다 |
key(n) |
n 번째 키의 이름을 돌려준다 |
length |
담긴 키의 개수. 메서드가 아니라 속성이다 |
화면 설정을 적고, 읽고, 지우는 코드를 봅니다. 줄 오른쪽 주석이 그 줄이 돌려주는 값입니다.
localStorage.setItem('theme', 'dark');
localStorage.getItem('theme'); // 'dark'
localStorage.getItem('lang'); // null
localStorage.length; // 1
localStorage.removeItem('theme');
localStorage.getItem('theme'); // null
없는 키를 읽어도 오류가 나지 않고 null 이 돌아옵니다. 처음 온 방문자에게도 null 이 돌아가므로, 코드는 null 이면 기본값을 쓰는 갈래를 함께 둡니다.
문자열만 담는 저장소
setItem 에 문자열이 아닌 값을 넘기면 브라우저가 문자열로 바꿔서 담습니다. 숫자 3 은 문자열 '3' 이 됩니다. 꺼낼 때 숫자로 돌아오지 않습니다.
객체는 더 곤란합니다. 객체를 문자열로 바꾸면 내용 대신 '[object Object]' 라는 문자열이 됩니다. 장바구니 객체를 넘겼다면 꺼낸 값에는 상품도 개수도 남지 않습니다.
그래서 객체는 JSON(JavaScript Object Notation, 자바스크립트 객체 표기법) 문자열로 바꿔 넣습니다. 넣을 때는 JSON.stringify 로 문자열을 만듭니다. 꺼낼 때는 JSON.parse 로 객체를 되살립니다.
객체를 이렇게 문자열이나 바이트로 바꾸는 일을 직렬화라고 합니다. 저장하거나 네트워크로 보내려면 메모리 속 객체를 한 줄의 데이터로 펼쳐야 합니다.
아래 코드에서 ls 는 localStorage 를 짧게 부르려고 담아 둔 변수입니다.
const ls = localStorage;
ls.setItem('count', 3);
ls.getItem('count'); // '3'
const cart = { id: 7, qty: 2 };
ls.setItem('cart', cart);
ls.getItem('cart'); // '[object Object]'
ls.setItem('cart', JSON.stringify(cart));
ls.getItem('cart'); // '{"id":7,"qty":2}'
JSON.parse(ls.getItem('cart')).qty; // 2
다섯째·여섯째 줄은 객체를 바꾸지 않고 넘긴 결과입니다. 일곱째 줄부터는 JSON 으로 바꿔 넣었으므로, 꺼내서 되살린 뒤 qty 를 숫자로 다시 읽습니다.
JSON 으로 옮기지 못하는 값도 있습니다. 날짜 객체는 문자열로 바뀌어서 되살려도 날짜가 아니라 문자열입니다. 함수는 아예 빠집니다.
세 가지 브라우저 저장소
브라우저에는 로컬 스토리지 말고도 값을 남길 곳이 있습니다. 흔히 견주는 것이 세션 스토리지와 쿠키입니다. 셋은 값이 언제 사라지는지와 서버로 실려 가는지에서 갈립니다.
쿠키는 서버가 브라우저에 맡겨 두는 작은 문자열입니다. 브라우저는 그 서버에 요청을 보낼 때마다 쿠키를 함께 실어 보냅니다. 로컬 스토리지의 값은 페이지 코드가 꺼내서 보내지 않는 한 서버로 가지 않습니다.
세션 스토리지는 로컬 스토리지와 메서드가 같습니다. 다른 것은 수명과 범위입니다. 값은 탭 하나에 묶입니다. 그 탭을 닫으면 값도 사라집니다.
메서드가 같은 이 두 저장소를 묶어 웹 스토리지라고 부릅니다. 웹 스토리지라는 말이 보이면 이 둘을 가리킨다고 읽으면 됩니다.
셋을 나란히 놓으면 아래 표와 같습니다.
| 로컬 스토리지 | 세션 스토리지 | 쿠키 | |
|---|---|---|---|
| 언제 사라지나 | 코드나 방문자가 지울 때 | 탭을 닫을 때 | 정해 둔 만료 시각 · 없으면 브라우저를 닫을 때 |
| 누가 함께 보나 | 같은 출처의 모든 탭 | 그 탭 하나 | 그 서버로 가는 모든 요청 |
| 서버로 가나 | 안 간다 | 안 간다 | 요청마다 간다 |
| 스크립트가 읽나 | 읽는다 | 읽는다 | HttpOnly 표시를 붙이면 못 읽는다 |
HttpOnly 는 쿠키를 스크립트에 안 보이게 하는 표시입니다. 이 표시가 왜 필요한지는 아래 「비밀값을 두면 생기는 일」 소절에서 다룹니다.
기다리는 호출과 메인 스레드
로컬 스토리지의 메서드는 결과를 돌려줄 때까지 다음 줄로 넘어가지 않습니다. 이렇게 결과가 나올 때까지 기다리는 호출을 동기 호출이라고 합니다.
페이지의 코드와 화면 그리기는 실행 흐름 하나를 나눠 씁니다. 이 실행 흐름을 메인 스레드라고 부릅니다. 메인 스레드에서 동기 호출이 오래 걸리면 그동안 클릭도 스크롤도 처리되지 않습니다. 작은 값 몇 개는 티가 안 나지만, 큰 값을 자주 읽고 쓰면 화면이 끊깁니다.
화면이 멈추지 않게 하려면 오래 걸리는 일을 메인 스레드 밖으로 넘깁니다. 그렇게 메인 스레드 밖에서 실행되는 스크립트를 웹 워커라고 합니다. 그런데 웹 워커 안에서는 로컬 스토리지를 쓸 수 없습니다. 로컬 스토리지는 페이지의 코드에서만 부를 수 있습니다.
서비스 워커도 메인 스레드 밖에서 실행되는 스크립트입니다. 페이지의 네트워크 요청을 가로채서, 오프라인일 때 저장해 둔 응답을 돌려주는 데 씁니다. 이 스크립트도 로컬 스토리지를 쓸 수 없습니다.
워커에서 값을 남기려면 브라우저 안의 데이터베이스인 IndexedDB를 씁니다. IndexedDB 는 페이지에서도 워커에서도 부를 수 있습니다.
IndexedDB 의 호출은 결과를 기다리지 않고 곧바로 돌아옵니다. 결과는 준비되면 나중에 따로 받습니다. 이런 호출을 비동기 호출이라고 합니다. 메인 스레드에서 불러도 화면을 붙잡지 않습니다.
담는 양과 지워지는 때
로컬 스토리지는 큰 데이터를 담으려고 만든 저장소가 아닙니다. 출처마다 몇 메가바이트 정도로 담는 양이 묶여 있습니다. 한도는 브라우저가 정합니다.
한도를 넘겨 적으려 하면 setItem 이 QuotaExceededError 라는 오류를 던집니다. 이 오류를 받아 처리하지 않으면 그 뒤의 코드가 실행되지 않습니다. 그래서 큰 값을 적는 코드는 try 로 감쌉니다.
값은 저절로 만료되지 않습니다. 코드가 지우지 않으면 몇 달 뒤에도 남아 있습니다. 대신 방문자가 브라우저 설정에서 저장된 데이터를 지우면 함께 사라집니다. 그래서 잃으면 안 되는 원본은 서버에 둡니다.
다른 탭에 가는 storage 이벤트
같은 출처의 탭들은 저장 공간을 함께 씁니다. 그래서 한 탭이 값을 바꾸면 다른 탭이 그 변화를 알아야 할 때가 있습니다. 한 탭에서 화면 설정을 바꿨을 때 다른 탭이 옛 설정으로 남아 있는 경우입니다.
브라우저는 이럴 때 같은 출처의 다른 탭에 storage 이벤트를 보냅니다. 이벤트는 무슨 일이 일어났다고 브라우저가 코드에 알리는 신호입니다. 이 이벤트에는 바뀐 키와 바뀌기 전 값, 바뀐 뒤 값이 실려 옵니다. 값을 바꾼 탭 자신은 이 이벤트를 받지 않습니다.
아래 그림은 탭 1 이 화면 설정을 light 로 바꾸는 순서입니다. 탭 2 는 이벤트를 받아 밝은 화면으로 다시 그립니다.
sequenceDiagram
participant 탭1 as 탭 1
participant 브라우저
participant 탭2 as 탭 2
탭1->>브라우저: setItem('theme', 'light')
브라우저->>브라우저: 저장 공간의 값을 바꾼다
브라우저->>탭2: storage 이벤트 · theme · dark → light
Note over 탭1: 탭 1 에는 이벤트가 가지 않는다
탭2->>탭2: 밝은 화면으로 다시 그린다
비밀값을 두면 생기는 일
로컬 스토리지의 값은 같은 출처에서 실행되는 스크립트라면 모두 읽습니다. 페이지가 직접 짠 코드뿐 아니라 함께 불러온 외부 라이브러리도 읽습니다.
공격자가 남의 페이지에 자기 스크립트를 끼워 넣는 공격이 XSS(Cross-Site Scripting, 교차 사이트 스크립팅)입니다. 끼워 넣은 스크립트도 그 출처에서 실행되는 셈이라 로컬 스토리지를 읽습니다. 로그인 토큰을 여기 두었다면 그 토큰이 공격자에게 넘어갑니다.
쿠키에 HttpOnly 표시를 붙이면 브라우저는 그 쿠키를 요청에 실어 보내기만 합니다. 스크립트에는 보이지 않습니다. 끼워 넣은 스크립트가 있어도 그 쿠키에 든 토큰은 읽어 가지 못합니다.
HttpOnly 쿠키에도 약점이 있습니다. 방문자가 내 서비스에 로그인한 채로 공격자가 만든 페이지를 열었다고 해 봅시다. 그 페이지에 내 서비스로 요청을 보내는 양식이 숨어 있으면, 브라우저는 그 요청에도 내 서비스의 쿠키를 실어 보냅니다. 이 점을 노리는 공격이 크로스 사이트 요청 위조입니다.
정리하면 로그인 토큰은 로컬 스토리지보다 HttpOnly 쿠키에 두는 설계가 흔합니다. 스크립트가 토큰을 아예 못 읽게 하려는 것입니다. 대신 크로스 사이트 요청 위조를 막는 장치를 따로 둡니다.
모바일 앱에도 로컬 스토리지처럼 작은 설정을 적어 두는 저장소가 있습니다. 하지만 토큰 같은 비밀값은 그곳 대신 운영체제가 주는 비밀 보관함에 둡니다. 애플 기기의 키체인이 그 예입니다.
쓰는 곳과 안 쓰는 곳
다시 와도 남아 있어야 하는 작은 설정이 로컬 스토리지에 맞습니다. 어두운 화면 여부, 고른 언어, 최근 본 상품 몇 개가 그런 값입니다. 쓰다 만 글을 잠깐 남겨 두는 데도 씁니다.
서버가 알아야 하는 값은 여기 두지 않습니다. 로컬 스토리지의 값은 서버로 저절로 가지 않으므로, 요청마다 필요한 값은 쿠키나 요청 헤더로 보냅니다.
크고 구조가 있는 데이터는 IndexedDB 에 둡니다. IndexedDB 는 객체를 문자열로 바꾸지 않고 담습니다. 결과를 나중에 받으므로 큰 데이터를 다뤄도 화면이 멈추지 않습니다.
서버 디스크를 가리키는 로컬 스토리지
서버 분야에서는 같은 낱말이 다른 것을 가리킵니다. 서버에 직접 꽂힌 디스크를 로컬 스토리지라고 부릅니다. 네트워크 너머에 따로 두고 붙여 쓰는 저장 장치는 네트워크 스토리지라고 부릅니다. 이 뜻은 스토리지 항목이 다룹니다.
관련 항목
로컬 스토리지가 속하는 상위 분류
웹 스토리지 · Web API · HTML Living Standard · 브라우저 · DOM
로컬 스토리지와 같은 역할을 두고 겨루는 브라우저 저장소
세션 스토리지 · 쿠키 · IndexedDB · 캐시 스토리지 · OPFS · Web SQL · 브라우저 캐시
로컬 스토리지에 값을 넣고 꺼낼 때 거치는 변환
문자열 · JSON · 직렬화 · 역직렬화 · 구조화된 복제
로컬 스토리지의 범위와 한도를 정하는 규칙
동일 출처 정책 · 오리진 · 스토리지 할당량 · 스토리지 분할 · 영속 스토리지 · QuotaExceededError
로컬 스토리지를 부르는 브라우저 실행 환경
JavaScript · 메인 스레드 · 이벤트 루프 · 이벤트 · storage 이벤트 · 웹 워커 · 서비스 워커 · 비동기
로컬 스토리지에 둔 비밀을 노리는 공격과 막는 수단
XSS · 콘텐츠 보안 정책 · HttpOnly · 크로스 사이트 요청 위조 · 토큰 · 액세스 토큰 · 리프레시 토큰 · JWT
다른 플랫폼에서 같은 역할을 하는 저장소
UserDefaults · SharedPreferences · 키체인 · 안드로이드 키스토어
로컬 스토리지에 값을 남기는 프런트엔드 설계
상태 관리 · 오프라인 우선 · 프로그레시브 웹 앱 · 키-값 저장소
같은 이름을 쓰는 서버 쪽 저장 장치
스토리지 · 네트워크 스토리지 · DAS · NAS · SAN
다른 이름: localStorage · Local Storage · window.localStorage · 로컬스토리지