← 블로그 목록
localStorageIndexedDB브라우저 저장소오프라인프론트엔드

브라우저 저장소 3종 정리: localStorage·sessionStorage·IndexedDB 어디에 무엇을

세 저장소의 수명·용량·동기 여부 차이를 정리하고, 테마 설정부터 오프라인 데이터·인증 토큰까지 무엇을 어디에 둬야 하는지 표로 비교했습니다.

Vibeollio 팀-

클라이언트에 무엇을 어디에 저장할까

브라우저에 데이터를 남겨야 하는 상황은 자주 옵니다. 다크모드 설정, 작성 중인 글, 오프라인에서도 보여야 하는 목록 같은 것들입니다. 선택지는 세 가지이고 성격이 꽤 다릅니다.

셋 다 출처(origin) 단위로 격리된다는 공통점이 있습니다. 프로토콜·도메인·포트가 하나라도 다르면 다른 저장소입니다. 이 전제 위에서 차이를 봅니다.

localStorage: 가장 단순하고, 그래서 함정이 있다

문자열 키에 문자열 값을 넣는 저장소입니다. 브라우저를 닫아도 남고, 명시적으로 지우거나 사용자가 사이트 데이터를 삭제할 때까지 유지됩니다.

localStorage.setItem("theme", "dark");
const theme = localStorage.getItem("theme"); // 없으면 null

주의할 점이 세 가지 있습니다.

문자열만 저장됩니다. 객체는 JSON.stringify로 직렬화해야 하고, 읽을 때 JSON.parse가 실패할 수 있습니다. 저장 형식을 바꿨을 때 옛 값이 남아 있으면 파싱에서 터집니다.

동기 API입니다. 읽고 쓰는 동안 메인 스레드가 멈춥니다. 값 몇 개는 문제없지만, 큰 JSON을 매 렌더마다 읽고 쓰면 체감되는 지연이 생깁니다.

접근 자체가 예외를 던질 수 있습니다. 사이트 데이터를 차단한 브라우저나 일부 임베드 환경에서는 localStorage를 참조하는 순간 오류가 납니다. 값이 없는 것과 접근이 막힌 것은 다른 상황이므로 항상 감싸야 합니다.

function readTheme() {
  try {
    return localStorage.getItem("theme");
  } catch {
    return null; // 저장소를 못 쓰는 환경 — 기본값으로 동작해야 한다
  }
}

용량은 브라우저마다 다르지만 출처당 수 MB 수준으로 보는 것이 안전합니다. 그리고 다른 탭에서 값이 바뀌면 storage 이벤트가 발생하므로, 탭 간 동기화가 필요하면 이걸 쓸 수 있습니다.

sessionStorage: 탭이 닫히면 사라진다

API는 localStorage와 동일합니다. 다른 점은 수명과 범위입니다. 탭(정확히는 브라우징 컨텍스트) 단위로 격리되고, 탭을 닫으면 지워집니다.

같은 사이트를 두 탭에서 열면 서로 다른 sessionStorage를 봅니다. 이게 장점이 되는 경우가 있습니다. 여러 단계로 이어지는 입력 폼의 중간 상태, 방금 어디서 들어왔는지 같은 값은 탭마다 독립적인 편이 자연스럽습니다.

새로고침에는 살아남지만 탭 복원 동작은 브라우저마다 차이가 있어, 반드시 남아야 하는 데이터를 여기 두면 안 됩니다.

IndexedDB: 구조화된 데이터를 제대로 담는다

비동기 트랜잭션 기반 데이터베이스입니다. 배웠던 것보다 쓰기가 번거롭지만, 나머지 둘로는 안 되는 것들이 됩니다.

  • 객체를 그대로 저장합니다. 직렬화가 필요 없고, Blob이나 ArrayBuffer도 넣을 수 있습니다. 이미지나 파일을 클라이언트에 보관하는 용도가 여기 해당합니다.
  • 인덱스로 질의합니다. 전체를 읽어와 필터링하는 대신 조건으로 찾을 수 있습니다.
  • 비동기입니다. 메인 스레드를 막지 않아 큰 데이터를 다뤄도 화면이 멈추지 않습니다.
  • 용량이 훨씬 큽니다. 정확한 한도는 브라우저와 디스크 여유에 따라 달라지지만, localStorage와는 자릿수가 다릅니다.

기본 API가 이벤트 기반이라 코드가 길어지므로, 실무에서는 얇은 래퍼 라이브러리를 얹는 경우가 많습니다.

// 얇은 래퍼(idb 등)를 쓰면 이 정도로 줄어든다
const db = await openDB("drafts", 1, {
  upgrade(db) { db.createObjectStore("posts", { keyPath: "id" }); },
});
await db.put("posts", { id: "1", body: "작성 중...", updatedAt: Date.now() });

용량이 커도 브라우저가 공간을 회수해야 할 때 지울 수 있다는 점은 기억해야 합니다. 지워지면 안 되는 데이터라면 navigator.storage.persist()로 영구 저장을 요청할 수 있지만, 승인 여부는 브라우저가 정합니다. 결론적으로 클라이언트 저장소는 어느 것도 서버 저장을 대체하지 못합니다.

쿠키는 여기 끼지 않는다

쿠키는 저장소라기보다 요청마다 자동으로 서버에 전송되는 값입니다. 클라이언트에서만 쓰는 상태를 쿠키에 담으면 모든 요청에 불필요한 바이트가 붙습니다. 용량 제한도 훨씬 작습니다.

반대로 서버가 알아야 하는 것은 쿠키가 맞습니다. 세션 식별자가 대표적입니다. 서버 렌더링에서 첫 화면부터 반영해야 하는 설정(예: 테마)도 쿠키에 두면 깜빡임 없이 처리할 수 있습니다. localStorage는 자바스크립트가 실행된 뒤에야 읽히기 때문입니다.

무엇을 어디에

데이터 위치
테마, 언어, 사이드바 접힘 상태 localStorage (서버 렌더링이 필요하면 쿠키)
여러 단계 폼의 중간 입력 sessionStorage
작성 중인 초안, 오프라인 목록, 캐시한 파일 IndexedDB
세션 식별자, 인증 토큰 HttpOnly 쿠키
서버 응답 캐시 Cache API (서비스 워커)

마지막 줄을 하나 강조하면, 인증 토큰을 localStorage에 두지 마세요. 자바스크립트가 읽을 수 있다는 뜻은 주입된 스크립트도 읽을 수 있다는 뜻입니다. HttpOnly 쿠키는 스크립트가 접근할 수 없습니다.

저장한 값이 사라지는 경우는 언제인가요?

여러 가지가 있습니다. 사용자가 사이트 데이터를 지운 경우, 시크릿 모드로 접속한 경우, 다른 브라우저나 다른 기기로 접속한 경우, 그리고 브라우저가 저장 공간을 회수한 경우입니다. 또 출처가 바뀌면 다른 저장소가 되므로, 도메인을 옮기거나 서브도메인을 바꾸면 이전 데이터를 볼 수 없습니다. 즉 "있으면 쓰고 없으면 기본값으로 동작한다"가 전제여야 합니다.

iframe 안에서는 왜 저장이 안 되나요?

sandbox 속성이 걸린 iframe에서 allow-same-origin이 빠져 있으면 문서가 출처 없는 상태로 취급되어, 출처에 묶인 저장소 API가 전부 막힙니다. 접근하는 순간 예외가 납니다. 그래서 임베드될 가능성이 있는 페이지라면 저장소 접근을 try/catch로 감싸고, 저장이 실패해도 기능이 계속 돌아가게 만들어야 합니다.

localStorage를 IndexedDB로 옮겨야 할 시점은?

세 가지 신호가 있습니다. 저장하는 데이터가 커져서 읽고 쓸 때 지연이 느껴질 때, 전체를 불러와 필터링하는 코드가 생겼을 때, 그리고 파일이나 이미지를 담아야 할 때입니다. 설정 값 몇 개 수준이라면 옮길 이유가 없습니다.

실제 환경에서 확인하는 것이 빠르다

저장소 관련 문제는 개발 환경에서 잘 드러나지 않습니다. 항상 같은 브라우저, 항상 데이터가 남아 있는 상태로 테스트하기 때문입니다. 시크릿 창으로 한 번 열어보면 첫 방문자가 보는 화면이 확인됩니다.

만든 프로젝트가 돌아가는 상태라면 Vibeollio에 프로젝트 등록해서 공개해보세요. 여러 사람이 각자 다른 브라우저와 설정으로 접속하면, 저장된 값에 의존하던 코드가 어디서 깨지는지 드러나는 경우가 많습니다.