오프라인 데이터

안정적인 오프라인 환경을 빌드하려면 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 래퍼인 제이크 아치볼드의 idb 라이브러리를 사용합니다. IndexedDB를 사용하기 위해 도우미 라이브러리는 필요하지 않지만 Promise 문법을 사용하려면 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를 연 후 다음과 같은 화면이 표시됩니다.

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

데이터 가져오는 중

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의 스토리지를 관리하는 방법을 아는 것은 네트워크 응답을 올바르게 저장하고 스트리밍하는 데 특히 중요합니다.

스토리지 용량은 캐시 저장소, IndexedDB, 웹 저장소, 서비스 워커 파일 및 종속 항목을 비롯한 모든 스토리지 옵션 간에 공유됩니다. 그러나 사용 가능한 저장용량은 브라우저마다 다릅니다. 데이터가 부족할 가능성은 없습니다. 사이트는 일부 브라우저에 메가바이트 또는 기가바이트 단위의 데이터를 저장할 수 있습니다. 예를 들어 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의 애플리케이션 탭에서 저장용량 섹션을 열면 사이트의 할당량과 저장용량 사용량을 사용 항목별로 분류하여 확인할 수 있습니다.

애플리케이션의 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 기반 브라우저는 사용자에게 콘텐츠의 중요성을 판단하는 휴리스틱에 따라 지속성을 부여하거나 거부합니다. 예를 들어 Google Chrome의 한 가지 기준은 PWA 설치입니다. 사용자가 운영체제에 PWA 아이콘을 설치한 경우 브라우저에서 영구 저장소를 부여할 수 있습니다.

Mozilla Firefox에서 사용자에게 저장소 지속성 권한을 요청합니다.

API 브라우저 지원

웹 스토리지

Browser Support

  • Chrome: 4.
  • Edge: 12.
  • Firefox: 3.5.
  • Safari: 4.

Source

파일 시스템 액세스

Browser Support

  • Chrome: 86.
  • Edge: 86.
  • Firefox: 111.
  • Safari: 15.2.

Source

저장용량 관리자

Browser Support

  • Chrome: 55.
  • Edge: 79.
  • Firefox: 57.
  • Safari: 15.2.

Source

리소스