Kiwix PWA를 통해 사용자가 오프라인에서 사용하기 위해 인터넷의 데이터를 GB 단위로 저장할 수 있는 방법

Geoffrey Kantaris
Geoffrey Kantaris
Stéphane Coillet-Matillon
Stéphane Coillet-Matillon

왼쪽에 플라스틱 의자가 있는 간단한 테이블 위에 노트북이 놓여 있고 사람들이 노트북 주위에 모여 있습니다. 배경이 개발도상국의 학교처럼 보입니다.

이 사례 연구에서는 비영리단체인 Kiwix가 프로그레시브 웹 앱 기술과 File System Access API를 사용하여 사용자가 오프라인에서 사용할 대용량 인터넷 보관 파일을 다운로드하고 저장할 수 있도록 지원하는 방법을 살펴봅니다. Kiwix PWA 내의 새로운 브라우저 기능인 Origin Private File System(OPFS)을 다루는 코드의 기술적 구현에 대해 알아보세요. 이 기능은 파일 관리를 개선하고 권한 메시지 없이 보관 파일에 액세스할 수 있도록 지원합니다. 이 문서에서는 새로운 파일 시스템의 당면 과제에 대해 설명하고 향후 개발될 수 있는 가능성을 집중적으로 살펴봅니다.

Kiwix 정보

웹이 탄생한 지 30년이 지났지만 국제전기통신연합에 따르면 전 세계 인구의 3분의 1이 아직 안정적인 인터넷 액세스를 기다리고 있습니다. 여기에서 이야기가 끝나나요? 물론 아닐 것입니다. 스위스 기반 비영리단체인 Kiwix는 인터넷 액세스가 제한적이거나 아예 없는 사람들에게 지식을 제공하는 것을 목표로 오픈소스 앱 및 콘텐츠 생태계를 개발했습니다. 사용자가 인터넷에 쉽게 액세스할 수 없는 경우 다른 사용자가 연결이 가능한 시점과 위치에서 사용자를 대신해 주요 리소스를 다운로드하고 나중에 오프라인에서 사용할 수 있도록 로컬에 저장할 수 있다는 아이디어입니다. 이제 위키피디아, 프로젝트 구텐베르크, Stack Exchange, TED 강연 등 많은 중요한 사이트를 ZIM 파일이라는 고도로 압축된 보관 파일로 변환하고 Kiwix 브라우저에서 즉시 읽을 수 있습니다.

ZIM 보관 파일은 주로 HTML, JavaScript, CSS를 저장하는 데 고도로 효율적인 Zstandard(ZSTD) 압축(이전 버전에서는 XZ를 사용함)을 사용하고 이미지는 일반적으로 압축된 WebP 형식으로 변환됩니다. 각 ZIM에는 URL과 제목 색인도 포함됩니다. 여기서 압축이 중요합니다. 영어 위키백과 전체(640만 개의 도움말 및 이미지)가 ZIM 형식으로 변환된 후 97GB로 압축되기 때문입니다. 이는 모든 인간 지식의 합계가 이제 중급 Android 휴대전화에 들어갈 수 있다는 것을 깨닫기 전에는 많은 것처럼 들리지만 수학, 의학 등 테마별 버전의 위키백과 등 소규모 리소스도 많이 제공됩니다.

Kiwix는 데스크톱(Windows/Linux/macOS) 및 모바일(iOS/Android) 사용을 타겟팅하는 다양한 네이티브 앱을 제공합니다. 그러나 이 사례 연구에서는 최신 브라우저가 있는 모든 기기의 범용적이고 간단한 솔루션을 목표로 하는 프로그레시브 웹 앱(PWA)에 중점을 둡니다.

완전히 오프라인 상태에서 대용량 콘텐츠 보관 파일에 빠르게 액세스해야 하는 범용 웹 앱을 개발할 때 발생하는 문제를 살펴보고 이러한 문제에 대한 혁신적이고 흥미로운 솔루션을 제공하는 일부 최신 JavaScript API, 특히 File System Access APIOrigin Private File System을 살펴봅니다.

오프라인용 웹 앱을 사용 중이신가요?

Kiwix 사용자는 다양한 요구사항을 가진 다양한 사용자 집단이며 Kiwix는 사용자가 콘텐츠에 액세스하는 기기와 운영체제를 거의 또는 전혀 제어할 수 없습니다. 특히 저소득 국가에서는 이러한 기기 중 일부가 느리거나 오래되었을 수 있습니다. Kiwix는 최대한 많은 사용 사례를 다루려고 노력하면서도 모든 기기에서 가장 보편적인 소프트웨어인 웹브라우저를 사용하면 더 많은 사용자에게 도달할 수 있다는 점을 깨달았습니다. 따라서 JavaScript로 작성할 수 있는 모든 애플리케이션은 결국 JavaScript로 작성됨을 언급하는 애트우드의 법칙에서 영감을 받아 약 10년 전 Kiwix 개발자 중 일부는 Kiwix 소프트웨어를 C++에서 JavaScript로 포팅하기 시작했습니다.

이 포팅의 첫 번째 버전인 Kiwix HTML5는 현재 지원 중단된 Firefox OS 및 브라우저 확장 프로그램을 위한 것이었습니다. 핵심은 Emscripten 컴파일러를 사용하여 ASM.js의 중간 JavaScript 언어로 컴파일된 C++ 압축 해제 엔진(XZ 및 ZSTD)이었습니다. 나중에 Wasm 또는 WebAssembly로 컴파일되었습니다. 나중에 Kiwix JS로 이름이 변경된 브라우저 확장 프로그램은 여전히 활발하게 개발되고 있습니다.

Kiwix JS 오프라인 브라우저

프로그레시브 웹 앱(PWA)을 입력합니다. 이 기술의 잠재력을 인지한 Kiwix 개발자는 Kiwix JS의 전용 PWA 버전을 빌드하고 앱이 특히 오프라인 사용, 설치, 파일 처리, 파일 시스템 액세스 등의 영역에서 네이티브와 유사한 기능을 제공할 수 있도록 하는 OS 통합을 추가하기 시작했습니다.

오프라인 우선 PWA는 매우 가볍기 때문에 모바일 인터넷 연결이 간헐적이거나 비용이 많이 드는 환경에 적합합니다. 이를 지원하는 기술은 Kiwix JS를 기반으로 하는 모든 앱에서 사용하는 Service Worker API 및 관련 Cache API입니다. 이러한 API를 사용하면 앱이 서버 역할을 하여 조회 중인 기본 문서나 문서의 가져오기 요청을 가로채 (JS) 백엔드로 리디렉션하여 ZIM 보관 파일에서 응답을 추출하고 구성할 수 있습니다.

어디서나 사용할 수 있는 스토리지

ZIM 보관 파일의 크기가 크기 때문에 특히 휴대기기에서 저장소에 보관 파일을 저장하고 액세스하는 것이 Kiwix 개발자에게 가장 큰 골칫거리일 수 있습니다. 많은 Kiwix 최종 사용자는 인터넷에 연결되어 있을 때 나중에 오프라인에서 사용할 수 있도록 인앱 콘텐츠를 다운로드합니다. 다른 사용자는 토렌트를 사용하여 PC에서 다운로드한 후 휴대기기나 태블릿 기기로 전송하고, 모바일 인터넷이 불안정하거나 비용이 많이 드는 지역에서는 USB 스틱이나 휴대용 하드 드라이브에 콘텐츠를 교환합니다. 사용자에게 액세스 가능한 임의의 위치에서 콘텐츠에 액세스하는 이러한 모든 방법은 Kiwix JS 및 Kiwix PWA에서 지원해야 합니다.

Kiwix JS가 메모리가 부족한 기기에서도 수백 GB(ZIM 보관 파일 1개가 166GB임)의 대규모 보관 파일을 읽을 수 있게 된 것은 File API 덕분입니다. 이 API는 아주 오래된 브라우저를 비롯한 모든 브라우저에서 보편적으로 지원되므로 최신 API가 지원되지 않는 경우 범용 대체 역할을 합니다. Kiwix의 경우, HTML에서 input 요소를 정의하는 것만큼 쉽습니다.

<input
  type="file"
  accept="application/octet-stream,.zim,.zimaa,.zimab,.zimac, ..."
  value="Select folder with ZIM files"
  id="archiveFilesLegacy"
  multiple
/>

선택되면 입력 요소는 기본적으로 저장소의 기본 데이터를 참조하는 메타데이터인 File 객체를 보유합니다. 기술적으로 Kiwix의 객체 지향 백엔드는 순수 클라이언트 측 JavaScript로 작성되어 필요에 따라 대용량 보관 파일의 작은 슬라이스를 읽습니다. 이러한 슬라이스를 압축 해제해야 하는 경우 백엔드는 Wasm 압축 해제기에 전달하고 전체 blob (일반적으로 기사 또는 애셋)이 압축 해제될 때까지 요청이 있으면 추가 슬라이스를 가져옵니다. 즉, 대용량 보관 파일을 메모리에 완전히 읽을 필요가 없습니다.

File API는 범용이지만, File API에는 Kiwix JS 앱이 네이티브 앱에 비해 불편하고 구식처럼 보이게 하는 단점이 있습니다. 이 API를 사용하면 한 세션에서 다음 세션으로 액세스 권한을 유지할 방법이 없으므로 앱이 실행될 때마다 사용자가 파일 선택 도구를 사용하여 보관 파일을 선택하거나 앱에 파일을 드래그 앤 드롭해야 합니다.

많은 개발자와 마찬가지로 Kiwix JS 개발자는 이러한 불만족스러운 UX를 완화하기 위해 처음에는 Electron 경로를 선택했습니다. ElectronJS는 Node API를 사용하여 파일 시스템에 대한 전체 액세스를 비롯하여 강력한 기능을 제공하는 놀라운 프레임워크입니다. 하지만 잘 알려진 몇 가지 단점이 있습니다.

  • 데스크톱 운영체제에서만 실행됩니다.
  • 크기가 크고 무겁습니다(70MB~100MB).

모든 앱에 Chromium의 전체 사본이 포함되어 있기 때문에 Electron 앱의 크기는 최소화되고 번들로 묶인 PWA의 단 5.1MB와 비교할 때 매우 불리합니다.

Kiwix에서 PWA 사용자의 상황을 개선할 방법이 있었나요?

File System Access API가 구원해 줍니다.

2019년경 Kiwix는 Chrome 78에서 오리진 트라이얼을 진행한 후 Native File System API라는 새로운 API를 알게 되었습니다. 파일 또는 폴더의 파일 핸들을 가져와서 IndexedDB 데이터베이스에 저장하는 기능을 약속했습니다. 결정적으로 이 핸들은 앱 세션 간에 유지되므로 사용자가 앱을 다시 실행할 때 파일 또는 폴더를 다시 선택하지 않아도 됩니다 (단, 빠른 권한 메시지에 응답해야 함). 프로덕션에 도달할 때까지 이름이 File System Access API로 변경되었으며 핵심 부분은 WHATWG에서 File System API(FSA)로 표준화되었습니다.

그렇다면 API의 파일 시스템 액세스 부분은 어떻게 작동하나요? 몇 가지 주의할 중요한 점은 다음과 같습니다.

  • 비동기 API입니다 (웹 작업자의 특수 함수 제외).
  • 파일 또는 디렉터리 선택 도구는 사용자 동작(UI 요소 클릭 또는 탭)을 캡처하여 프로그래매틱 방식으로 실행해야 합니다.
  • 사용자가 새 세션에서 이전에 선택한 파일에 액세스할 수 있는 권한을 다시 부여하려면 사용자 동작도 필요합니다. 실제로 브라우저는 사용자 동작으로 시작하지 않으면 권한 메시지 표시를 거부합니다.

코드는 파일 및 디렉터리 핸들을 저장하기 위해 불편한 IndexedDB API를 사용해야 하는 점을 제외하고는 비교적 간단합니다. 다행히 browser-fs-access와 같이 많은 작업을 대신해 주는 라이브러리가 몇 가지 있습니다. Kiwix JS에서는 문서가 잘 작성된 API를 직접 사용하기 위해

파일 및 디렉터리 선택 도구 열기

파일 선택 도구를 여는 방법은 다음과 같습니다(여기서는 Promises를 사용하지만 async/await 문법을 선호하는 경우 개발자용 Chrome 튜토리얼을 참고하세요).

return window
  .showOpenFilePicker({ multiple: false })
  .then(function (fileHandles) {
    return processFileHandle(fileHandles[0]);
  })
  .catch(function (err) {
    // This is normal if app is launching
    console.warn(
      'User cancelled, or cannot access fs without user gesture',
      err,
    );
  });

편의상 이 코드는 선택한 첫 번째 파일만 처리하며 두 개 이상 선택하는 것은 허용하지 않습니다. { multiple: true }로 여러 파일을 선택할 수 있도록 하려면 각 핸들을 처리하는 모든 프로미스를 Promise.all().then(...) 문에 래핑하면 됩니다. 예를 들면 다음과 같습니다.

let promisesForFiles = fileHandles.map(function (fileHandle) {
    return processFileHandle(fileHandle);
});
return Promise.all(promisesForFiles).then(function (arrayOfFiles) {
    // Do something with the files array
    console.log(arrayOfFiles);
}).catch(function (err) {
    // Handle any errors that occurred during processing
    console.error('Error processing file handles!', err);
)};

그러나 여러 파일을 선택하는 경우 사용자에게 개별 파일이 아닌 해당 파일이 포함된 디렉터리를 선택하도록 요청하는 것이 더 좋습니다. 특히 Kiwix 사용자는 모든 ZIM 파일을 동일한 디렉터리에 정리하는 경향이 있기 때문입니다. 디렉터리 선택 도구를 실행하는 코드는 window.showDirectoryPicker.then(function (dirHandle) { … });를 사용하는 점을 제외하고 위와 거의 동일합니다.

파일 또는 디렉터리 핸들 처리

핸들을 가져오면 이를 처리해야 하므로 processFileHandle 함수는 다음과 같이 표시될 수 있습니다.

function processFileHandle(fileHandle) {
  // Serialize fileHandle to indexedDB
  serializeFSHandletoIdxDB('pickedFSHandle', fileHandle, function (val) {
    console.debug('IndexedDB responded with ' + val);
  });
  return fileHandle.getFile().then(function (file) {
    // Do something with the file
    return file;
  });
}

파일 핸들을 저장하는 함수를 제공해야 합니다. 추상화 라이브러리를 사용하지 않는 한 이를 위한 편의 메서드는 없습니다. Kiwix의 구현은 cache.js 파일에서 확인할 수 있지만 파일이나 폴더 핸들을 저장하고 검색하는 데만 사용되는 경우 상당히 간단해질 수 있습니다.

디렉터리 처리는 원하는 파일 또는 파일 형식을 찾기 위해 선택한 디렉터리의 항목을 비동기 entries.next()로 반복해야 하므로 조금 더 복잡합니다. 이를 수행하는 방법에는 여러 가지가 있지만 Kiwix PWA에서 사용되는 코드는 다음과 같습니다.

let iterableEntryList = dirHandle.entries();
return iterateAsyncDirEntries(iterableEntryList, []).then(function (entryList) {
  // Do something with the entry list
  return entryList;
});

/**
 * Iterates FileSystemDirectoryHandle iterator and adds entries to an array
 * @param {Iterator} entries An asynchronous iterator of entries
 * @param {Array} archives An array to which to add the entries (may be empty)
 * @return {Promise<Array>} A Promise for an array of entries in the directory
 */
function iterateAsyncDirEntries(entries, archives) {
  return entries
    .next()
    .then(function (result) {
      if (!result.done) {
        let entry = result.value[1];
        // Filter for the files you want
        if (/\.zim(\w\w)?$/i.test(entry.name)) {
          archives.push(entry);
        }
        return iterateAsyncDirEntryArray(entries, archives);
      } else {
        // We've processed all the entries
        if (!archives.length) {
          console.warn('No archives found in the picked directory!');
        }
        return archives;
      }
    })
    .catch(function (err) {
      console.error('There was an error processing the directory!', err);
    });
}

entryList의 각 항목에서 나중에 파일을 사용해야 할 때 entry.getFile().then(function (file) { … })로 파일을 가져오거나 async function에서 const file = await entry.getFile()를 사용하여 이에 상응하는 파일을 가져와야 합니다.

더 자세히 알아볼까요?

후속 앱 실행 시 사용자 동작으로 시작된 권한을 부여해야 하는 요구사항은 파일 및 폴더 (재)열기에 약간의 불편을 주지만 파일을 다시 선택해야 하는 것보다는 훨씬 원활합니다. Chromium 개발자는 현재 설치된 PWA에 지속적인 권한을 허용하는 코드를 마무리하고 있습니다. 이는 많은 PWA 개발자가 요청해 왔으며 많은 기대를 모으고 있습니다.

하지만 기다릴 필요가 없는 경우도 있습니다. Kiwix 개발자는 최근 Chromium 및 Firefox 브라우저에서 모두 지원되고 Safari에서 부분적으로 지원되지만 여전히 FileSystemWritableFileStream이 누락된 File Access API의 새로운 기능을 사용하여 지금 바로 모든 권한 메시지를 제거할 수 있음을 발견했습니다. 이 새로운 기능은 출처 비공개 파일 시스템입니다.

완전히 네이티브로 전환: 출처 비공개 파일 시스템

출처 비공개 파일 시스템(OPFS)은 아직 Kiwix PWA의 실험용 기능이지만 네이티브 앱과 웹 앱 간의 격차를 크게 해소하므로 에서는 사용자에게 이를 사용해 보라고 적극 권장하고 있습니다. 주요 이점은 다음과 같습니다.

  • OPFS의 보관 파일은 실행 시에도 권한 메시지 없이 액세스할 수 있습니다. 사용자는 이전 세션에서 중단한 지점부터 아무런 불편함 없이 기사 읽기와 보관 파일 탐색을 재개할 수 있습니다.
  • 여기에 저장된 파일에 대한 고도로 최적화된 액세스를 제공합니다. Android에서는 속도가 5~10배 빨라집니다.

File API를 사용하는 Android의 표준 파일 액세스는 매우 느립니다. 특히 대용량 보관 파일이 기기 저장소가 아닌 microSD 카드에 저장된 경우(Kiwix 사용자의 경우가 많음) 더욱 느려집니다. 이 새로운 API를 사용하면 모든 것이 달라집니다. 대부분의 사용자는 OPFS(microSD 카드 저장소가 아닌 기기 저장소를 사용함)에 97GB 파일을 저장할 수 없지만, 소형~중형 보관 파일을 저장하는 데 적합합니다. 위키프로젝트 의학에서 가장 완벽한 의학 백과사전을 확인하고 싶으신가요? 괜찮습니다. 1.7GB이므로 OPFS에 쉽게 들어갑니다. (도움말: 인앱 라이브러리에서 othermdwiki_ko_all_maxi를 찾습니다.)

OPFS 작동 방식

OPFS는 브라우저에서 제공하는 파일 시스템으로, 출처마다 별개이며 Android의 앱 범위 스토리지와 유사하다고 생각할 수 있습니다. 사용자는 볼 수 있는 파일 시스템에서 OPFS로 파일을 가져오거나 직접 다운로드할 수 있습니다 (API를 통해 OPFS에서 파일을 만들 수도 있음). OPFS에 있으면 기기의 나머지 부분과 격리됩니다. 데스크톱 Chromium 기반 브라우저에서는 OPFS에서 사용자에게 표시되는 파일 시스템으로 파일을 다시 내보낼 수도 있습니다.

OPFS를 사용하려면 먼저 navigator.storage.getDirectory()를 사용하여 액세스를 요청해야 합니다. 다시 말하지만 await를 사용하는 코드를 보려면 출처 비공개 파일 시스템을 참고하세요.

return navigator.storage
  .getDirectory()
  .then(function (handle) {
    return processDirHandle(handle);
  })
  .catch(function (err) {
    console.warn('Unable to get the OPFS directory entry', err);
  });

여기에서 가져오는 핸들은 위에 언급된 window.showDirectoryPicker()에서 가져오는 FileSystemDirectoryHandle와 동일한 유형이므로 이를 처리하는 코드를 재사용할 수 있습니다. 다행히 이를 indexedDB에 저장할 필요는 없습니다. 필요할 때 가져오면 됩니다. OPFS에 이미 파일이 있고 이를 사용하려는 경우 이전에 표시된 iterateAsyncDirEntries() 함수를 사용하여 다음과 같이 할 수 있습니다.

return navigator.storage.getDirectory().then(function (dirHandle) {
  let entries = dirHandle.entries();
  return iterateAsyncDirEntries(entries, [])
    .then(function (archiveList) {
      return archiveList;
    })
    .catch(function (err) {
      console.error('Unable to iterate OPFS entries', err);
    });
});

archiveList 배열에서 작업하려는 항목에는 여전히 getFile()를 사용해야 합니다.

OPFS로 파일 가져오기

그렇다면 애초에 파일을 OPFS로 가져오려면 어떻게 해야 할까요? 그렇게 서두르지 마세요! 먼저 처리해야 할 스토리지 양을 추정하고 사용자가 스토리지에 맞지 않는 97GB 파일을 넣지 못하도록 해야 합니다.

예상 할당량을 확인하는 방법은 간단합니다. navigator.storage.estimate().then(function (estimate) { … });. 사용자에게 이를 표시하는 방법을 찾는 것이 조금 더 어렵습니다. Kiwix 앱에서는 체크박스 바로 옆에 작은 인앱 패널이 표시되어 사용자가 OPFS를 사용해 볼 수 있도록 했습니다.

사용된 스토리지의 비율(%)과 사용 가능한 남은 스토리지(GB)를 보여주는 패널

패널은 estimate.quotaestimate.usage를 사용하여 채워집니다. 예를 들면 다음과 같습니다.

let OPFSQuota; // Global variable, so we don't have to keep checking it
return navigator.storage.estimate().then(function (estimate) {
  const percent = ((estimate.usage / estimate.quota) * 100).toFixed(2);
  OPFSQuota = estimate.quota - estimate.usage;
  document.getElementById('OPFSQuota').innerHTML =
    '<b>OPFS storage quota:</b><br />Used:&nbsp;<b>' +
    percent +
    '%</b>; ' +
    'Remaining:&nbsp;<b>' +
    (OPFSQuota / 1024 / 1024 / 1024).toFixed(2) +
    '&nbsp;GB</b>';
});

보시다시피 사용자가 사용자에게 표시되는 파일 시스템에서 OPFS에 파일을 추가할 수 있는 버튼도 있습니다. 다행히 File API를 사용하여 가져올 파일 객체(또는 객체)를 가져올 수 있습니다. 사실 window.showOpenFilePicker()는 Firefox에서 지원되지 않지만 OPFS는 확실히 지원되므로 window.showOpenFilePicker()를 사용하지 않는 것이 중요합니다.

위의 스크린샷에 표시된 파일 추가 버튼은 기존 파일 선택기가 아니지만 클릭하거나 탭하면 숨겨진 기존 선택기(<input type="file" multiple … /> 요소)를 click()합니다. 그러면 앱은 숨겨진 파일 입력의 change 이벤트를 캡처하고 파일 크기를 확인한 후 할당량이 너무 크면 거부합니다. 문제가 없다면 사용자에게 추가할지 묻습니다.

archiveFilesLegacy.addEventListener('change', function (files) {
  const filesArray = Array.from(files.target.files);
  // Abort if user didn't select any files
  if (filesArray.length === 0) return;
  // Calculate the size of the picked files
  let filesSize = 0;
  filesArray.forEach(function (file) {
    filesSize += file.size;
  });
  // Check the size of the files does not exceed the quota
  if (filesSize > OPFSQuota) {
    // Oh no, files are too big! Tell user...
    console.log('Files would exceed the OPFS quota!');
  } else {
    // Ask user if they're sure... if user said yes...
    return importOPFSEntries(filesArray)
      .then(function () {
        // Tell user we successfully imported the archives
      })
      .catch(function (err) {
        // Tell user there was an error (error catching is important!)
      });
  }
});

사용자에게 원본 비공개 파일 시스템에 .zim 파일 목록을 추가할지 묻는 대화상자

Android와 같은 일부 운영체제에서는 보관 파일 가져오기가 가장 빠른 작업이 아니므로 Kiwix는 보관 파일이 가져오는 동안 배너와 작은 스피너도 표시합니다. 팀에서 이 작업에 진행률 표시기를 추가하는 방법을 찾지 못했습니다. 해결 방법을 찾으면 엽서에 답장해 주세요.

Kiwix는 importOPFSEntries() 함수를 어떻게 구현했나요? 이를 위해서는 fileHandle.createWriteable() 메서드를 사용해야 하며, 이 메서드를 사용하면 각 파일을 OPFS로 효과적으로 스트리밍할 수 있습니다. 모든 어려운 작업은 브라우저에서 처리합니다. 여기서 Kiwix는 기존 코드베이스와 관련된 이유로 프로미스를 사용하고 있지만, 이 경우에는 await가 더 간단한 문법을 생성하고 운명의 피라미드를 피해야 합니다.

function importOPFSEntries(files) {
  // Get a handle on the OPFS directory
  return navigator.storage
    .getDirectory()
    .then(function (dir) {
      // Collect the promises for each file that we want to write
      let promises = files.map(function (file) {
        // Create the file and get a writeable handle on it
        return dir
          .getFileHandle(file.name, { create: true })
          .then(function (fileHandle) {
            // Get a writer for the file
            return fileHandle.createWritable().then(function (writer) {
              // Show a banner / spinner, then write the file
              return writer
                .write(file)
                .then(function () {
                  // Finished with this writer
                  return writer.close();
                })
                .catch(function (err) {
                  console.error('There was an error writing to the OPFS!', err);
                });
            });
          })
          .catch(function (err) {
            console.error('Unable to get file handle from OPFS!', err);
          });
      });
      // Return a promise that resolves when all the files have been written
      return Promise.all(promises);
    })
    .catch(function (err) {
      console.error('Unable to get a handle on the OPFS directory!', err);
    });
}

파일 스트림을 OPFS로 직접 다운로드

이에 대한 변형은 인터넷에서 OPFS로 직접 파일을 스트리밍하거나 디렉터리 핸들이 있는 디렉터리(window.showDirectoryPicker()로 선택한 디렉터리)로 파일을 스트리밍하는 기능입니다. 위의 코드와 동일한 원칙을 사용하지만 ReadableStream 및 원격 파일에서 읽은 바이트를 큐에 추가하는 컨트롤러로 구성된 Response를 생성합니다. 그러면 결과 Response.body가 OPFS 내의 새 파일 작성자에게 파이프됩니다.

이 경우 Kiwix는 ReadableStream를 통과하는 바이트를 계산하여 사용자에게 진행률 표시기를 제공하고 다운로드 중에 앱을 종료하지 말라고 경고할 수 있습니다. 코드는 여기에서 표시하기에는 너무 복잡하지만 앱이 FOSS 앱이므로 유사한 작업을 하고 싶다면 소스를 살펴보세요. Kiwix UI는 다음과 같습니다. 아래에 표시된 다양한 진행률 값은 백분율이 변경될 때만 배너를 업데이트하지만 다운로드 진행률 패널은 더 자주 업데이트하기 때문입니다.

하단에 앱을 종료하지 말라는 경고와 .zim 보관 파일의 다운로드 진행률을 표시하는 막대가 있는 Kiwix 사용자 인터페이스

다운로드하는 데 상당한 시간이 걸릴 수 있으므로 Kiwix에서는 작업 중에 앱을 자유롭게 사용할 수 있지만, 다운로드 작업이 완료될 때까지 앱을 닫지 말라고 사용자에게 알릴 수 있도록 배너가 항상 표시되도록 할 수 있습니다.

인앱 미니 파일 관리자 구현

이 시점에서 Kiwix PWA 개발자는 OPFS에 파일을 추가하는 것만으로는 충분하지 않다는 것을 깨달았습니다. 또한 앱은 사용자에게 이 저장소 영역에서 더 이상 필요하지 않은 파일을 삭제하는 방법을 제공해야 했으며, 가능하면 OPFS에 잠긴 파일을 사용자에게 표시되는 파일 시스템으로 다시 내보내야 했습니다. 사실상 앱 내부에 미니 파일 관리 시스템을 구현하는 것이 필수가 되었습니다.

Chrome용 멋진 OPFS Explorer 확장 프로그램(Edge에서도 작동)을 소개합니다. 개발자 도구에 탭을 추가하여 OPFS에 있는 항목을 정확하게 확인하고 잘못되거나 실패한 파일을 삭제할 수 있습니다. 코드가 작동하는지 확인하고, 다운로드 동작을 모니터링하고, 개발 실험을 일반적으로 정리하는 데 매우 유용했습니다.

파일 내보내기는 Kiwix가 내보낸 파일을 저장할 선택한 파일 또는 디렉터리의 파일 핸들을 가져오는 기능에 종속되므로 window.showSaveFilePicker() 메서드를 사용할 수 있는 컨텍스트에서만 작동합니다. Kiwix 파일이 몇 GB 미만이면 메모리에 blob을 생성하고 URL을 지정한 후 사용자에게 표시되는 파일 시스템에 다운로드할 수 있습니다. 안타깝게도 이렇게 큰 보관 파일은 불가능합니다. 지원되는 경우 내보내는 것은 매우 간단합니다. OPFS에 파일을 저장하는 것과 거의 동일하지만 반대입니다(저장할 파일의 핸들을 가져오고 사용자에게 window.showSaveFilePicker()를 사용하여 저장할 위치를 선택하도록 요청한 다음 saveHandle에서 createWriteable()를 사용). 저장소에서 코드를 확인할 수 있습니다.

파일 삭제는 모든 브라우저에서 지원되며 간단한 dirHandle.removeEntry('filename')로 실행할 수 있습니다. Kiwix의 경우에는 선택된 파일이 먼저 있는지 확인하고 확인을 요청할 수 있도록 위에서 했던 것처럼 OPFS 항목을 반복하는 것이 좋지만 모든 사용자에게 필요하지 않을 수 있습니다. 관심이 있으시면 Google의 코드를 살펴보세요.

이러한 옵션을 제공하는 버튼으로 Kiwix UI를 어지럽히지 않고 대신 보관처 목록 바로 아래에 작은 아이콘을 배치하기로 결정했습니다. 이러한 아이콘 중 하나를 탭하면 사용자가 수행할 작업에 관한 시각적 단서가 되어 보관처리 목록의 색상이 변경됩니다. 그런 다음 사용자가 보관처리 중 하나를 클릭하거나 탭하면 확인 후 해당 작업(내보내기 또는 삭제)이 실행됩니다.

사용자에게 .zim 파일을 삭제할지 묻는 대화상자

마지막으로 위에 설명된 모든 파일 관리 기능(OPFS에 파일 추가, OPFS에 파일 직접 다운로드, 파일 삭제, 사용자에게 표시되는 파일 시스템으로 내보내기)을 보여주는 스크린캐스트 데모를 확인해 보세요.

개발자의 작업은 끝이 없습니다.

OPFS는 PWA 개발자를 위한 혁신적인 기능으로, 네이티브 앱과 웹 앱 간의 격차를 줄이는 데 큰 도움이 되는 강력한 파일 관리 기능을 제공합니다. 하지만 개발자는 불만족스러운 집단입니다. 결코 만족하지 못합니다. OPFS는 거의 완벽하지만 완벽하지는 않습니다. 주요 기능이 Chromium과 Firefox 브라우저 모두에서 작동하며 데스크톱은 물론 Android에서도 구현된다는 점도 좋습니다. 곧 Safari 및 iOS에도 전체 기능 세트가 구현될 수 있기를 바랍니다. 다음과 같은 문제가 남아 있습니다.

  • Firefox는 현재 기본 디스크 공간의 크기에 관계없이 OPFS 할당량에 10GB의 한도를 설정합니다. 대부분의 PWA 작성자에게는 이 한도가 충분할 수 있지만 Kiwix의 경우에는 매우 제한적입니다. 다행히 Chromium 브라우저는 훨씬 관대합니다.
  • 현재 window.showSaveFilePicker()가 구현되지 않았으므로 대용량 파일을 OPFS에서 모바일 브라우저 또는 데스크톱 Firefox에서 사용자에게 표시되는 파일 시스템으로 내보낼 수 없습니다. 이러한 브라우저에서는 대용량 파일이 사실상 OPFS에 갇히게 됩니다. 이는 콘텐츠에 대한 자유로운 액세스와 특히 간헐적이거나 비용이 많이 드는 인터넷 연결이 제공되는 지역에서 사용자 간에 보관 파일을 공유할 수 있는 Kiwix의 정신에 위배됩니다.
  • OPFS 가상 파일 시스템에서 사용할 스토리지를 사용자가 제어할 수 없습니다. 이는 사용자가 microSD 카드에 많은 공간을 보유하고 있지만 기기 저장소에는 매우 적은 공간을 보유한 휴대기기에서 특히 문제가 됩니다.

그러나 대체로 이들은 PWA의 파일 액세스에 큰 진전을 이루는 사소한 차이에 불과합니다. Kiwix PWA팀은 파일 시스템 액세스 API를 처음 제안하고 설계한 Chromium 개발자와 옹호자, 그리고 출처 비공개 파일 시스템의 중요성에 대해 브라우저 공급업체 간에 합의를 이루기 위해 노력해 주신 모든 분들께 감사드립니다. Kiwix JS PWA의 경우 과거에 앱을 망설이던 많은 UX 문제를 해결했으며 모든 사용자를 위해 Kiwix 콘텐츠의 접근성을 향상시키는 데 도움이 됩니다. Kiwix PWA를 사용해 보고 개발자에게 의견을 전달해 주세요.

PWA 기능에 관한 유용한 리소스는 다음 사이트를 참고하세요.