Haeminway haeminway
English
기술 노트로
6 분 분량

브라우저가 내 데이터를 버릴 때: 초록 체크가 거짓말을 한다

모바일 localStorage는 용량이 차면 데이터를 조용히 버린다. 그런데 '저장됨' 플래그는 남아 완료 체크가 뜬다. 저장소 계층별 용도, 큰 본문을 IndexedDB에 두는 법, 열 때 정합성 검사, storage.persist()로 eviction 줄이기까지.

브라우저 저장소는 durable하지 않다 — 데이터를 버리고도 “저장됨” 플래그는 남긴다. 모바일 localStorage에 사진을 넣으면, 용량이 찰 때 브라우저가 본문은 조용히 버리고 filled: true만 남긴다. 그럼 앱은 깨진 이미지 위에 초록 체크를 그려 사용자한테 거짓말을 한다.

localStorage는 용량이 차면 사진 본문을 버리고 filled 플래그만 남긴다. 본문은 IndexedDB로 옮겨야 한다

왜 중요한가

사용자는 완료된 줄 알고 다음으로 넘어간다. 실제 데이터는 없는데. 현장에서 사진을 다시 찍을 기회를 그 순간 놓친다. 그리고 이건 에러가 아니라 “성공처럼 보이는 실패”라 리포트도 늦게 온다.

어디에 무엇을 두나

브라우저 저장소는 하나가 아니다. 크기·특성이 달라서, 잘못 고르면 조용히 사라진다.

저장소용량특성용도
localStorage~5MB동기, 문자열만작은 플래그·설정
sessionStorage~5MB탭 한정, 닫으면 소멸일회성 임시
IndexedDB수백MB+비동기, Blob 저장사진·파일·초안 본문
Cache API대용량비동기네트워크 응답 캐시

핵심: 큰 본문(사진·바이너리)은 IndexedDB, 작은 플래그만 localStorage.

잘못된 방식: 본문을 localStorage에

// 큰 dataURL을 localStorage에 → 한도 초과 시 던지거나 다른 항목을 evict
try {
  localStorage.setItem("photo:" + id, bigDataUrl);   // 1~3MB
} catch (e) {
  // QuotaExceededError — 근데 이미 다른 사진이 밀려났을 수도 있다
}
localStorage.setItem("photo:" + id + ":filled", "true"); // 본문 없이 플래그만 남는 사고

filled=true는 성공했는데 본문 저장은 실패 — 이 두 줄이 따로 노는 순간 거짓 체크가 된다.

정답: 큰 본문은 IndexedDB, 상태는 본문 존재로 판정

본문은 IndexedDB에 넣고(비동기), 상태는 플래그가 아니라 본문이 실제로 있는지로 계산한다.

function openDb_() {
  return new Promise((resolve, reject) => {
    const open = indexedDB.open("drafts", 1);
    open.onupgradeneeded = () => open.result.createObjectStore("photos");
    open.onsuccess = () => resolve(open.result);
    open.onerror = () => reject(open.error);
  });
}

function idbPut_(key, blob) {                       // 본문 저장
  return openDb_().then((db) => new Promise((res, rej) => {
    const tx = db.transaction("photos", "readwrite");
    tx.objectStore("photos").put(blob, key);
    tx.oncomplete = () => res(true);
    tx.onerror = () => rej(tx.error);
  }));
}

function idbGet_(key) {                             // 본문 조회 (없으면 undefined)
  return openDb_().then((db) => new Promise((res, rej) => {
    const req = db.transaction("photos").objectStore("photos").get(key);
    req.onsuccess = () => res(req.result);
    req.onerror = () => rej(req.error);
  }));
}

// 상태는 '플래그'가 아니라 '본문이 실제로 있는가'로 판정한다
async function photoState_(key, meta) {
  const body = await idbGet_(key);
  if (body) return "filled";
  if (meta && meta.filled) return "missing-body";   // 플래그만 남고 본문은 사라짐
  return "empty";
}

열 때 정합성 검사 — 사라진 걸 사용자에게 알려라

앱을 열 때 플래그와 본문을 대조해서, 본문이 없는 항목을 missing-body로 뒤집는다.

// 앱 시작 시: 플래그는 있는데 본문이 없는 항목을 찾아 '다시 첨부'로 표시
async function reconcileDrafts_(metas) {
  const missing = [];
  for (const m of metas) {
    if (m.filled && !(await idbGet_(m.key))) {
      m.state = "missing-body";
      missing.push(m.key);
    }
  }
  if (missing.length) notifyUser_(missing.length + "개 사진을 다시 첨부해야 합니다");
  return metas;
}

가능하면 용량을 미리 보고, 영구 저장을 요청해 eviction 확률을 낮춘다.

// 여유 확인 + 영구 저장 요청 (자동 삭제 대상에서 빠질 확률↑, 보장은 아님)
const { usage, quota } = await navigator.storage.estimate();
if (navigator.storage.persist) await navigator.storage.persist();

놓치기 쉬운 것

  • 플래그와 데이터를 따로 믿지 마라. “저장됨”은 의도지 사실이 아니다. missing-body 같은 3번째 상태를 두고 본문 실재로 판정하라.
  • localStorage는 동기·소용량(~5MB). 큰 값을 넣으면 던지거나 조용히 실패한다. 사진·바이너리는 IndexedDB로.
  • 거짓 성공을 UI에 그리지 마라. 본문 없는 “완료”는 초록 체크가 아니라 빨간 “다시 첨부”로.
  • iOS Safari는 미방문 시 지운다. ITP상 일정 기간 안 열면 스크립트 저장소를 비운다. PWA일수록 persist() + 열 때 정합성 검사가 필수다.

깊이: 초안은 두 저장소로 나눠라

오프라인 초안은 메타데이터·커서를 localStorage, 사진·바이너리 본문을 IndexedDB에 나눠 담는다. 서버 저장이 끝난 뒤에만 로컬 본문을 지우고, 그 전엔 본문 실재로 상태를 판정한다. 핵심 한 줄: “저장됨” 플래그가 아니라 데이터의 실재로 상태를 판정하라.

자주 묻는 질문

localStorage에 사진을 저장했는데 왜 사라지나요?
localStorage는 대략 5MB로 작습니다. 압축한 사진 한 장이 한도를 넘으면 브라우저가 QuotaExceededError를 던지거나 조용히 저장에 실패합니다. 사진·파일 같은 큰 본문은 IndexedDB에 저장하세요.
PWA 초안을 다시 열면 왜 비어 있나요?
브라우저는 저장 공간이 부족하면 오리진 저장소를 통째로 비울 수 있습니다. iOS Safari는 일정 기간 미방문 시 데이터를 지우기도 합니다. 본문은 IndexedDB에 두고, 열 때마다 정합성을 검사해 사라진 항목을 사용자에게 알리세요.
데이터가 없는데 완료 체크가 뜨는 이유는?
'저장됨' 플래그만 남고 실제 본문은 evict된 상태입니다. 플래그와 데이터를 따로 믿으면 이렇게 거짓 성공을 그립니다. 상태를 본문의 실재로 판정하세요.
브라우저가 내 데이터를 안 지우게 할 수 있나요?
navigator.storage.persist()로 영구 저장을 요청하면 용량 압박 시 자동 삭제 대상에서 빠질 확률이 높아집니다. 다만 보장은 아니므로, 그래도 열 때 정합성 검사는 필요합니다.