오프라인 데이터

안정적인 오프라인 환경을 빌드하려면 PWA에 저장소 관리가 필요합니다. 캐싱 챕터에서는 캐시 저장소가 기기에 데이터를 저장하는 한 가지 옵션이라는 것을 배웠습니다. 이 장에서는 데이터 지속성, 제한 및 사용 가능한 도구를 포함하여 오프라인 데이터를 관리하는 방법에 대해 설명합니다.

스토리지

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

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

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

네트워크 리소스(HTML, CSS, JavaScript, 이미지, 동영상, 오디오 등 URL을 통해 요청하여 액세스하는 네트워크 리소스)에는 Cache Storage API를 사용합니다.

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

IndexedDB

IndexedDB를 사용하려면 먼저 데이터베이스를 엽니다. 이렇게 하면 데이터베이스가 없는 경우 새 데이터베이스가 생성됩니다. IndexedDB는 비동기 API이지만 Promise를 반환하는 대신 콜백을 사용합니다. 다음 예에서는 IndexedDB를 위한 작은 Promise 래퍼인 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');
     }
   }
  });
}

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

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

Safari 및 Chrome에 IndexedDB 콘텐츠가 표시되어 있음

데이터 추가

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에 의해 자동으로 설정되고 증가합니다. 이 코드를 두 번 실행하면 동일한 쿠키 항목이 2개 생성됩니다.

데이터 검색

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 브라우저 지원

웹 스토리지

브라우저 지원

  • 4
  • 12
  • 3.5
  • 4

소스

파일 시스템 액세스

브라우저 지원

  • 86
  • 86
  • 111
  • 15.2

소스

저장용량 관리자

브라우저 지원

  • 55
  • 79
  • 57
  • 15.2

소스

자료