오프라인 데이터

안정적인 오프라인 환경을 구축하려면 PWA에 저장용량 관리가 필요합니다. 캐싱 장에서는 캐시 저장소가 기기에 데이터를 저장하는 한 가지 옵션이라는 것을 배웠습니다. 이 장에서는 데이터 지속성, 한도, 사용 가능한 도구 등 오프라인 데이터를 관리하는 방법에 대해 알아봅니다.

스토리지는 파일 및 애셋뿐 아니라 다른 유형의 데이터도 포함할 수 있습니다. PWA를 지원하는 모든 브라우저에서 다음 API를 기기 내 저장소로 사용할 수 있습니다.

  • IndexedDB: 구조화된 데이터 및 blob (바이너리 데이터)을 위한 NoSQL 객체 스토리지 옵션입니다.
  • WebStorage: 로컬 저장소 또는 세션 저장소를 사용하여 키-값 문자열 쌍을 저장하는 방법입니다. 서비스 워커 컨텍스트 내에서 사용할 수 없습니다. 이 API는 동기식이므로 복잡한 데이터 스토리지에는 권장되지 않습니다.
  • 캐시 저장소: 캐싱 모듈에서 다룹니다.
를 통해 개인정보처리방침을 정의할 수 있습니다.

지원되는 플랫폼에서 Storage Manager API를 사용하여 모든 기기 저장용량을 관리할 수 있습니다. Cache Storage API 및 IndexedDB는 PWA용 영구 저장소에 대한 비동기 액세스를 제공하며 기본 스레드, 웹 작업자, 서비스 워커에서 액세스할 수 있습니다. 둘 다 네트워크가 불안정하거나 존재하지 않을 때 PWA가 안정적으로 작동하도록 하는 데 필수적인 역할을 합니다. 하지만 어떤 경우에 각 모델을 사용해야 할까요?

네트워크 리소스(HTML, CSS, JavaScript, 이미지, 동영상, 오디오 등)를 통해 요청하여 액세스할 수 있는 항목의 경우 Cache Storage API를 사용합니다.

IndexedDB를 사용하여 구조화된 데이터를 저장합니다. 여기에는 NoSQL과 같은 방식으로 검색 또는 결합 가능해야 하는 데이터나 URL 요청과 반드시 일치하지는 않는 사용자별 데이터 등이 포함됩니다. IndexedDB는 전체 텍스트 검색용으로 설계되지 않았습니다.

IndexedDB

IndexedDB를 사용하려면 먼저 데이터베이스를 여세요. 이렇게 하면 데이터베이스가 없는 경우 새 데이터베이스가 생성됩니다. IndexedDB는 비동기 API이지만 프로미스를 반환하는 대신 콜백을 사용합니다. 다음 예에서는 IndexedDB용 작은 프라미스 래퍼인 Jake Archibald의 idb 라이브러리를 사용합니다. IndexedDB를 사용하는 데 도우미 라이브러리가 필요하지는 않지만 프로미스 문법을 사용하고 싶다면 idb 라이브러리를 사용하면 됩니다.

다음 예에서는 요리 레시피를 보관할 데이터베이스를 만듭니다.

데이터베이스 만들기 및 열기

데이터베이스를 열려면 다음 안내를 따르세요.

  1. openDB 함수를 사용하여 cookbook라는 새 IndexedDB 데이터베이스를 만듭니다. IndexedDB 데이터베이스는 버전이 지정되므로 데이터베이스 구조를 변경할 때마다 버전 번호를 높여야 합니다. 두 번째 매개변수는 데이터베이스 버전입니다. 이 예에서는 1로 설정되어 있습니다.
  2. upgrade() 콜백이 포함된 초기화 객체가 openDB()에 전달됩니다. 콜백 함수는 데이터베이스가 처음 설치되거나 새 버전으로 업그레이드될 때 호출됩니다. 이 함수에서만 작업이 발생할 수 있습니다. 작업에는 새 객체 저장소 (IndexedDB가 데이터를 구성하는 데 사용하는 구조) 또는 색인 (검색하려는 항목) 만들기가 포함될 수 있습니다. 데이터 이전도 이루어져야 합니다. 일반적으로 upgrade() 함수에는 break 문이 없는 switch 문이 포함되어 있어 이전 버전의 데이터베이스에 따라 각 단계가 순서대로 발생할 수 있습니다.
import { openDB } from 'idb';

async function createDB() {
  // Using https://github.com/jakearchibald/idb
  const db = await openDB('cookbook', 1, {
    upgrade(db, oldVersion, newVersion, transaction) {
      // Switch over the oldVersion, *without breaks*, to allow the database to be incrementally upgraded.
    switch(oldVersion) {
     case 0:
       // Placeholder to execute when database is created (oldVersion is 0)
     case 1:
       // Create a store of objects
       const store = db.createObjectStore('recipes', {
         // The `id` property of the object will be the key, and be incremented automatically
           autoIncrement: true,
           keyPath: 'id'
       });
       // Create an index called `name` based on the `type` property of objects in the store
       store.createIndex('type', 'type');
     }
   }
  });
}

이 예시에서는 id 속성을 매장의 색인 키로 설정하여 recipes라는 cookbook 데이터베이스 내에 객체 저장소를 만들고 type 속성을 기반으로 type라는 또 다른 색인을 만듭니다.

방금 만든 객체 저장소를 살펴보겠습니다. 객체 저장소에 레시피를 추가하고 Chromium 기반 브라우저에서 DevTools 또는 Safari의 Web Inspector를 열면 다음과 같이 표시됩니다.

IndexedDB 콘텐츠를 표시하는 Safari 및 Chrome

데이터 추가

IndexedDB는 트랜잭션을 사용합니다. 트랜잭션은 작업을 함께 그룹화하여 하나의 단위로 발생합니다. 데이터베이스가 항상 일관된 상태를 유지하도록 해줍니다. 또한 실행 중인 앱 사본이 여러 개 있는 경우 동일한 데이터에 동시에 쓰지 않도록 하는 데 매우 중요합니다. 데이터를 추가하려면 다음 단계를 따르세요.

  1. modereadwrite로 설정하여 트랜잭션을 시작합니다.
  2. 데이터를 추가할 객체 저장소를 가져옵니다.
  3. 저장 중인 데이터로 add()를 호출합니다. 이 메서드는 사전 형식의 데이터를 키/값 쌍으로 받아 객체 저장소에 추가합니다. 사전은 구조화된 클론을 사용하여 클론 가능해야 합니다. 기존 객체를 업데이트하려면 put() 메서드를 대신 호출합니다.

트랜잭션에는 트랜잭션이 성공적으로 완료되거나 트랜잭션 오류와 함께 거부되면 확인되는 done 프로미스가 있습니다.

IDB 라이브러리 문서에 설명된 대로 데이터베이스에 쓰는 경우 tx.done은 모든 것이 데이터베이스에 성공적으로 커밋되었다는 신호입니다. 그러나 트랜잭션 실패를 유발하는 모든 오류를 확인할 수 있도록 개별 작업을 기다리는 것이 좋습니다.

// Using https://github.com/jakearchibald/idb
async function addData() {
  const cookies = {
      name: "Chocolate chips cookies",
      type: "dessert",
        cook_time_minutes: 25
  };
  const tx = await db.transaction('recipes', 'readwrite');
  const store = tx.objectStore('recipes');
  store.add(cookies);
  await tx.done;
}

쿠키를 추가하면 레시피가 다른 레시피와 함께 데이터베이스에 표시됩니다. ID는 indexDB에 의해 자동으로 설정되고 증가합니다. 이 코드를 두 번 실행하면 동일한 쿠키 항목이 두 개 생성됩니다.

데이터 가져오는 중

다음은 IndexedDB에서 데이터를 가져오는 방법입니다.

  1. 거래를 시작하고 객체 매장(선택사항)을 지정하고 거래 유형(선택사항)을 지정합니다.
  2. 이 거래에서 objectStore()를 호출합니다. 객체 스토어 이름을 지정해야 합니다.
  3. 가져오려는 키로 get()를 호출합니다. 기본적으로 저장소는 키를 색인으로 사용합니다.
// Using https://github.com/jakearchibald/idb
async function getData() {
  const tx = await db.transaction('recipes', 'readonly')
  const store = tx.objectStore('recipes');
// Because in our case the `id` is the key, we would
// have to know in advance the value of the id to
// retrieve the record
  const value = await store.get([id]);
}

스토리지 관리자

PWA의 저장소를 관리하는 방법을 아는 것은 네트워크 응답을 올바르게 저장하고 스트리밍하는 데 특히 중요합니다.

스토리지 용량은 Cache Storage, IndexedDB, Web Storage는 물론 서비스 워커 파일 및 해당 종속 항목을 포함한 모든 스토리지 옵션 간에 공유됩니다. 하지만 사용 가능한 저장용량은 브라우저마다 다릅니다. 소진될 가능성은 거의 없습니다. 사이트는 일부 브라우저에 메가바이트, 심지어 기가바이트의 데이터를 저장할 수 있습니다. 예를 들어 Chrome에서는 브라우저가 총 디스크 공간의 최대 80% 를 사용할 수 있으며, 개별 출처는 전체 디스크 공간의 최대 60% 를 사용할 수 있습니다. Storage API를 지원하는 브라우저의 경우 앱에서 사용할 수 있는 저장용량, 할당량, 사용량을 알 수 있습니다. 다음 예시에서는 Storage API를 사용하여 예상 할당량과 사용량을 가져온 후 사용된 비율과 남은 바이트를 계산합니다. navigator.storageStorageManager의 인스턴스를 반환합니다. 별도의 Storage 인터페이스가 있으므로 혼동하기 쉽습니다.

if (navigator.storage && navigator.storage.estimate) {
  const quota = await navigator.storage.estimate();
  // quota.usage -> Number of bytes used.
  // quota.quota -> Maximum number of bytes available.
  const percentageUsed = (quota.usage / quota.quota) * 100;
  console.log(`You've used ${percentageUsed}% of the available storage.`);
  const remaining = quota.quota - quota.usage;
  console.log(`You can write up to ${remaining} more bytes.`);
}

Chromium DevTools에서 Application 탭의 Storage 섹션을 열어 사이트의 할당량 및 저장용량을 사용 중인 항목별로 분류하여 확인할 수 있습니다.

애플리케이션의 Chrome DevTools, 저장용량 섹션 지우기

Firefox와 Safari는 현재 원본의 모든 저장용량 및 사용량을 볼 수 있는 요약 화면을 제공하지 않습니다.

데이터 지속성

비활성 상태 또는 저장용량 부족 시 데이터가 자동으로 제거되지 않도록 브라우저에 호환 플랫폼의 영구 저장소를 요청할 수 있습니다. 승인하는 경우 브라우저가 저장소에서 데이터를 삭제하지 않습니다. 이러한 보호에는 서비스 워커 등록, IndexedDB 데이터베이스, 캐시 스토리지에 있는 파일이 포함됩니다. 사용자가 항상 소유하고 있으며 브라우저에서 영구 저장용량을 허용했더라도 사용자가 언제든지 저장용량을 삭제할 수 있습니다.

영구 스토리지를 요청하려면 StorageManager.persist()를 호출합니다. 이전과 마찬가지로 StorageManager 인터페이스는 navigator.storage 속성을 통해 액세스합니다.

async function persistData() {
  if (navigator.storage && navigator.storage.persist) {
    const result = await navigator.storage.persist();
    console.log(`Data persisted: ${result}`);
}

StorageManager.persisted()를 호출하여 현재 원본에 영구 스토리지가 이미 부여되었는지 확인할 수도 있습니다. 영구 저장소를 사용하려면 Firefox에서 사용자에게 권한을 요청합니다. Chromium 기반 브라우저는 휴리스틱을 기반으로 지속성을 부여하거나 거부하여 사용자에게 콘텐츠의 중요도를 결정합니다. Chrome의 기준 중 하나는 PWA 설치입니다. 사용자가 운영체제에 PWA 아이콘을 설치한 경우 브라우저에서 영구 스토리지를 부여할 수도 있습니다.

사용자에게 저장소 지속성 권한을 요청하는 Mozilla Firefox

를 통해 개인정보처리방침을 정의할 수 있습니다.

API 브라우저 지원

웹 저장소

브라우저 지원

  • Chrome: 4. <ph type="x-smartling-placeholder">
  • Edge: 12. <ph type="x-smartling-placeholder">
  • Firefox: 3.5 <ph type="x-smartling-placeholder">
  • Safari: 4. <ph type="x-smartling-placeholder">

소스

파일 시스템 액세스

브라우저 지원

  • Chrome: 86 <ph type="x-smartling-placeholder">
  • Edge: 86. <ph type="x-smartling-placeholder">
  • Firefox: 111 <ph type="x-smartling-placeholder">
  • Safari 15.2. <ph type="x-smartling-placeholder">

소스

저장용량 관리자

브라우저 지원

  • Chrome: 55. <ph type="x-smartling-placeholder">
  • Edge: 79. <ph type="x-smartling-placeholder">
  • Firefox: 57 <ph type="x-smartling-placeholder">
  • Safari 15.2. <ph type="x-smartling-placeholder">

소스

리소스