IndexedDB 사용 권장사항

널리 사용되는 상태 관리 라이브러리인 IndexedDB 간에 애플리케이션 상태를 동기화하기 위한 권장사항을 알아봅니다.

사용자가 처음 웹사이트나 애플리케이션을 로드할 때는 UI를 렌더링하는 데 사용되는 초기 애플리케이션 상태를 구성하는 데 상당한 작업이 필요한 경우가 많습니다. 예를 들어 앱에서 사용자 클라이언트 측을 인증한 후 페이지에 표시해야 하는 모든 데이터를 얻기 전에 API 요청을 여러 번 해야 하는 경우가 있습니다.

IndexedDB에 애플리케이션 상태를 저장하면 반복 방문의 로드 시간을 단축할 수 있습니다. 그러면 앱은 백그라운드에서 모든 API 서비스와 동기화하고 비활성 기간 재검증 전략을 사용하여 새 데이터로 UI를 느리게 업데이트할 수 있습니다.

IndexedDB의 또 다른 유용한 용도는 사용자 제작 콘텐츠를 서버에 업로드하기 전에 임시 저장소로 저장하거나 원격 데이터의 클라이언트 측 캐시(또는 둘 다)로 저장하는 것입니다.

하지만 IndexedDB를 사용할 때 고려해야 할 중요한 사항이 API를 처음 사용하는 개발자에게는 바로 명확하지 않을 수 있습니다. 이 문서에서는 일반적인 질문에 대한 답변을 제공하고 IndexedDB에서 데이터를 유지할 때 유념해야 할 가장 중요한 몇 가지 사항에 대해 설명합니다.

앱을 예측 가능한 상태로 유지하기

IndexedDB와 관련된 많은 복잡성은 개발자가 제어할 수 없는 요소가 너무 많기 때문입니다. 이 섹션에서는 IndexedDB를 사용할 때 유의해야 하는 여러 문제에 대해 알아봅니다.

일부 플랫폼에서는 IndexedDB에 일부 데이터를 저장할 수 없음

이미지나 동영상과 같이 대용량의 사용자 생성 파일을 저장하는 경우 File 또는 Blob 객체로 저장하려고 할 수 있습니다. 일부 플랫폼에서는 작동하지만 다른 플랫폼에서는 실패합니다. 특히 iOS의 Safari에서는 IndexedDB에 Blob를 저장할 수 없습니다.

다행히 BlobArrayBuffer로 변환하는 것은 그리 어렵지 않으며 그 반대도 마찬가지입니다. IndexedDB에 ArrayBuffer를 저장하는 것은 매우 잘 지원됩니다.

그러나 Blob에는 MIME 유형이 있지만 ArrayBuffer에는 없는 유형이 있습니다. 올바르게 변환하려면 유형을 버퍼와 함께 저장해야 합니다.

ArrayBufferBlob로 변환하려면 Blob 생성자를 사용하면 됩니다.

function arrayBufferToBlob(buffer, type) {
  return new Blob([buffer], { type: type });
}

다른 방향은 약간 더 복잡하며 비동기식 프로세스입니다. FileReader 객체를 사용하여 blob을 ArrayBuffer로 읽을 수 있습니다. 읽기가 완료되면 리더에서 loadend 이벤트가 트리거됩니다. 다음과 같이 Promise에 이 프로세스를 래핑할 수 있습니다.

function blobToArrayBuffer(blob) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.addEventListener('loadend', () => {
      resolve(reader.result);
    });
    reader.addEventListener('error', reject);
    reader.readAsArrayBuffer(blob);
  });
}

스토리지에 쓰기가 실패할 수 있음

IndexedDB에 쓸 때 오류는 다양한 이유로 발생할 수 있으며 경우에 따라 이러한 오류는 개발자가 제어할 수 없습니다. 예를 들어 일부 브라우저는 현재 시크릿 브라우징 모드에서 IndexedDB에 쓰기를 허용하지 않습니다. 또한 사용자가 디스크 공간이 거의 소진된 기기를 사용하고 있을 가능성도 있으며, 브라우저에서 어떤 것도 저장하지 못하도록 제한합니다.

따라서 IndexedDB 코드에서 항상 적절한 오류 처리를 구현하는 것이 매우 중요합니다. 또한 일반적으로 애플리케이션 상태를 저장하는 것 외에도 메모리에 애플리케이션 상태를 보관하여 시크릿 브라우징 모드에서 실행되거나 저장공간을 사용할 수 없을 때(저장소가 필요한 일부 앱 기능이 작동하지 않더라도) UI가 중단되지 않도록 하는 것이 좋습니다.

IDBDatabase, IDBTransaction 또는 IDBRequest 객체를 만들 때마다 error 이벤트에 대한 이벤트 핸들러를 추가하여 IndexedDB 작업에서 오류를 포착할 수 있습니다.

const request = db.open('example-db', 1);
request.addEventListener('error', (event) => {
  console.log('Request error:', request.error);
};

저장된 데이터가 사용자에 의해 수정 또는 삭제되었을 수 있습니다.

무단 액세스를 제한할 수 있는 서버 측 데이터베이스와 달리 클라이언트 측 데이터베이스는 브라우저 확장 프로그램 및 개발자 도구에서 액세스할 수 있으며 사용자가 삭제할 수 있습니다.

사용자가 로컬에 저장된 데이터를 수정하는 경우는 드물지만 이 데이터를 삭제하는 것은 매우 일반적입니다. 애플리케이션에서 오류 없이 이 두 가지 경우를 모두 처리할 수 있어야 합니다.

저장된 데이터가 최신 상태가 아닐 수 있음

이전 섹션과 마찬가지로 사용자가 데이터 자체를 수정하지 않았더라도 스토리지에 있는 데이터가 이전 버전의 코드(예: 버그가 있는 버전)로 작성되었을 수도 있습니다.

IndexedDB는 스키마 버전과 IDBOpenDBRequest.onupgradeneeded() 메서드를 통한 업그레이드를 기본적으로 지원합니다. 하지만 이전 버전 (버그가 있는 버전 포함)에서 유입되는 사용자를 처리할 수 있도록 업그레이드 코드를 작성해야 합니다.

단위 테스트는 가능한 모든 업그레이드 경로와 사례를 수동으로 테스트하는 것이 불가능한 경우가 많으므로 여기에서는 매우 유용할 수 있습니다.

앱 성능 유지

IndexedDB의 주요 기능 중 하나는 비동기 API이지만 이 기능을 사용할 때 성능을 걱정할 필요가 없다는 생각에 속지 마세요. 부적절한 사용으로 인해 기본 스레드가 여전히 차단되어 버벅거림이 발생하고 응답하지 않는 경우가 많이 있습니다.

일반적으로 IndexedDB에 대한 읽기 및 쓰기는 액세스하는 데이터에 필요한 크기보다 클 수 없습니다.

IndexedDB를 사용하면 중첩된 대규모 객체를 단일 레코드로 저장할 수 있으며 이렇게 하는 것은 개발자의 관점에서 매우 편리합니다. 하지만 이 방법은 피해야 합니다. IndexedDB가 객체를 저장할 때 먼저 해당 객체의 구조화된 클론을 만들어야 하고 구조화된 클론 프로세스가 기본 스레드에서 발생하기 때문입니다. 객체가 클수록 차단 시간이 길어집니다.

따라서 애플리케이션 상태를 IndexedDB에 유지하는 방법을 계획할 때 문제가 발생합니다. 대부분의 인기 상태 관리 라이브러리 (예: Redux)가 전체 상태 트리를 단일 JavaScript 객체로 관리하여 작동하기 때문입니다.

이러한 방식으로 상태를 관리하면 많은 이점이 있지만 (예: 코드에서 추론 및 디버그가 쉬워짐) 전체 상태 트리를 IndexedDB에 단일 레코드로 저장하기만 하면 유혹과 편리할 수 있지만, 모든 변경 후에 이렇게 하면 (제한/디바운스된 경우에도) 기본 스레드가 불필요하게 차단되고 쓰기 오류가 발생할 가능성이 높아지고 브라우저가 응답하지 않는 경우도 발생할 수 있습니다.

전체 상태 트리를 단일 레코드에 저장하는 대신 개별 레코드로 나누고 실제로 변경되는 레코드만 업데이트해야 합니다.

이미지, 음악 또는 동영상과 같은 대용량 항목을 IndexedDB에 저장하는 경우에도 마찬가지입니다. 더 큰 객체 내부가 아닌 자체 키로 각 항목을 저장하면 바이너리 파일을 검색하는 비용을 지불하지 않고도 구조화된 데이터를 검색할 수 있습니다.

대부분의 권장사항과 마찬가지로 이는 '전부 아니면 전무' 방식의 규칙이 아닙니다. 상태 객체를 분할하고 최소한의 변경 집합만 작성할 수 없는 경우 데이터를 하위 트리로 분할한 다음 하위 트리로만 쓰는 것이 항상 전체 상태 트리를 작성하는 편이 좋습니다. 조금만 개선하면 개선이 아예 없는 것보다 낫습니다.

마지막으로, 항상 작성하는 코드의 성능 영향을 측정해야 합니다. IndexedDB에 대한 작은 쓰기가 대규모 쓰기보다 더 나은 성능을 발휘하는 것은 사실이지만 이는 애플리케이션이 수행하는 IndexedDB에 쓰기가 실제로 기본 스레드를 차단하고 사용자 환경을 저하하는 장기 작업으로 이어지는 경우에만 중요합니다. 최적화의 기준을 파악할 수 있도록 측정하는 것이 중요합니다

결론

개발자는 IndexedDB와 같은 클라이언트 스토리지 메커니즘을 활용하여 세션 간에 상태를 유지할 뿐만 아니라 재방문 시 초기 상태를 로드하는 데 걸리는 시간을 줄여 애플리케이션의 사용자 환경을 개선할 수 있습니다.

IndexedDB를 올바르게 사용하면 사용자 환경이 크게 개선되지만, IndexedDB를 잘못 사용하거나 오류 사례를 처리하지 못하면 앱이 손상되고 사용자가 불만족스러워할 수 있습니다.

클라이언트 저장소는 제어할 수 없는 많은 요소가 관련되어 있기 때문에 코드가 철저히 테스트되고, 초기에는 발생할 가능성이 낮은 경우에도 오류를 적절히 처리하는 것이 중요합니다.