원본 비공개 파일 시스템

File System Standard(파일 시스템 표준)는 OPFS(Origin Private File System)를 페이지 출처 전용 스토리지 엔드포인트로 도입하고, 사용자는 볼 수 없습니다. 이를 통해 성능에 고도로 최적화된 특별한 종류의 파일에 대한 선택적 액세스를 제공합니다.

브라우저 지원

원본 비공개 파일 시스템은 최신 브라우저에서 지원되며 WhatWG (Web Hypertext Application Technology Working Group) File System Living Standard에서 표준화되었습니다.

브라우저 지원

  • 86
  • 86
  • 111
  • 15.2

소스

동기

컴퓨터의 파일을 생각할 때는 파일 계층 구조, 즉 운영체제의 파일 탐색기로 탐색할 수 있는 폴더에 정리된 파일을 생각할 수 있습니다. 예를 들어 Windows에서 톰이라는 사용자의 경우 할 일 목록이 C:\Users\Tom\Documents\ToDo.txt에 있을 수 있습니다. 이 예에서 ToDo.txt은 파일 이름이고 Users, Tom, Documents은 폴더 이름입니다. Windows의 `C:` 는 드라이브의 루트 디렉터리를 나타냅니다.

웹에서 파일로 작업하는 기존 방식

웹 애플리케이션에서 할 일 목록을 수정하는 방법은 다음과 같습니다.

  1. 사용자가 파일을 서버에 업로드하거나 <input type="file">를 사용하여 클라이언트에서 엽니다.
  2. 사용자가 변경한 다음, JavaScript를 통해 프로그래매틱 방식으로 click() 삽입된 <a download="ToDo.txt>를 사용하여 결과 파일을 다운로드합니다.
  3. 폴더를 열려면 <input type="file" webkitdirectory>의 특수 속성을 사용합니다. 이 속성은 독점 이름에도 불구하고 사실상 범용 브라우저를 지원합니다.

웹에서 파일을 사용하는 최신 방식

이 흐름은 사용자가 파일 수정에 대해 생각하는 방식을 나타내지 않으며 사용자는 입력 파일의 사본을 다운로드하게 됩니다. 따라서 File System Access API에 도입된 세 가지 선택 도구 메서드, showOpenFilePicker(), showSaveFilePicker(), showDirectoryPicker()는 이름에서 알 수 있는 것과 정확히 일치하는 것입니다. 다음과 같이 흐름을 사용 설정합니다.

  1. showOpenFilePicker()ToDo.txt를 열고 FileSystemFileHandle 객체를 가져옵니다.
  2. FileSystemFileHandle 객체에서 파일 핸들의 getFile() 메서드를 호출하여 File를 가져옵니다.
  3. 파일을 수정한 다음 핸들에서 requestPermission({mode: 'readwrite'})를 호출합니다.
  4. 사용자가 권한 요청을 수락하면 변경사항을 다시 원본 파일에 저장합니다.
  5. 또는 showSaveFilePicker()를 호출하고 사용자가 새 파일을 선택하도록 합니다. (이전에 연 파일을 선택하면 해당 내용을 덮어씁니다.) 반복 저장의 경우 파일 핸들을 유지할 수 있으므로 파일 저장 대화상자를 다시 표시하지 않아도 됩니다.

웹에서 파일 작업 시 제한사항

이러한 메서드를 통해 액세스할 수 있는 파일과 폴더는 사용자에게 표시되는 파일 시스템에 상주합니다. 웹에서 저장된 파일 및 특히 실행 파일에는 웹 표시가 표시되므로 위험할 수 있는 파일이 실행되기 전에 운영체제에 추가 경고가 표시될 수 있습니다. 추가 보안 기능으로 웹에서 다운로드한 파일은 세이프 브라우징으로도 보호됩니다. 세이프 브라우징은 편의상 이 도움말의 맥락에서 클라우드 기반 바이러스 검사라고 생각하면 됩니다. File System Access API를 사용하여 파일에 데이터를 쓰면 쓰기가 제자리에 있지 않지만 임시 파일을 사용합니다. 이러한 보안 검사를 모두 통과하지 않으면 파일 자체가 수정되지 않습니다. 상상할 수 있듯이, 이러한 작업에서는 가능한 경우(예: macOS) 개선사항을 적용하더라도 파일 작업이 상대적으로 느려집니다. 여전히 모든 write() 호출은 독립적이므로 내부적으로 파일을 열고 지정된 오프셋을 찾은 다음 최종적으로 데이터를 씁니다.

처리의 기반으로 활용되는 Files

또한 파일은 데이터를 기록하는 좋은 방법입니다. 예를 들어 SQLite는 전체 데이터베이스를 단일 파일에 저장합니다. 또 다른 예로는 이미지 처리에 사용되는 밉맵이 있습니다. 밉맵은 미리 계산되고 최적화된 이미지 시퀀스로, 각 시퀀스는 이전 이미지를 점진적으로 낮은 해상도로 표현하므로 확대/축소와 같은 여러 작업이 더 빨라집니다. 그렇다면 웹 기반 파일 처리의 성능 비용 없이 웹 애플리케이션이 파일의 이점을 얻으려면 어떻게 해야 할까요? 정답은 바로 원본 비공개 파일 시스템입니다.

사용자에게 표시되는 파일 시스템과 원본 비공개 파일 시스템 비교

운영체제의 파일 탐색기를 사용하여 찾아볼 수 있는 사용자에게 표시되는 파일 시스템과 달리, 원본 비공개 파일 시스템은 사용자가 볼 수 없으며 읽고 쓰고 이동하고 이름을 변경할 수 있는 파일과 폴더가 있습니다. 원본 비공개 파일 시스템의 파일과 폴더는 이름에서 알 수 있듯이 비공개이며 더 구체적으로 사이트의 원본에 대해 비공개입니다. DevTools 콘솔에 location.origin를 입력하여 페이지의 출처를 확인합니다. 예를 들어 https://developer.chrome.com/articles/ 페이지의 출처가 https://developer.chrome.com입니다 (즉, 부분 /articles은 출처의 일부가 아님). 출처 이론에 대한 자세한 내용은 '동일 사이트' 및 '동일 출처' 이해하기를 참고하세요. 동일한 출처를 공유하는 모든 페이지는 동일한 출처 비공개 파일 시스템 데이터를 볼 수 있으므로 https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/는 이전 예와 동일한 세부정보를 볼 수 있습니다. 각 출처에는 자체적인 독립된 출처 비공개 파일 시스템이 있습니다. 즉, https://developer.chrome.com의 출처 비공개 파일 시스템은 https://web.dev와는 완전히 구분됩니다. Windows에서 사용자에게 표시되는 파일 시스템의 루트 디렉터리는 C:\\입니다. 원본 비공개 파일 시스템에 상응하는 것은 비동기 메서드 navigator.storage.getDirectory()를 호출하여 액세스하는 출처당 초기에 비어 있는 루트 디렉터리입니다. 사용자에게 표시되는 파일 시스템과 원본 비공개 파일 시스템의 비교는 다음 다이어그램을 참조하세요. 이 다이어그램은 루트 디렉터리를 제외하고 나머지 모든 항목이 개념적으로 동일하며, 데이터 및 스토리지 요구사항에 따라 필요한 대로 구성 및 배열할 파일 및 폴더의 계층 구조를 포함합니다.

두 가지 예시 파일 계층 구조를 보여주는, 사용자에게 표시되는 파일 시스템 및 원본 비공개 파일 시스템의 다이어그램 사용자에게 표시되는 파일 시스템의 진입점은 심볼릭 하드디스크이며, 원본 비공개 파일 시스템의 진입점은 &#39;navgator.storage.getDirectory&#39; 메서드를 호출합니다.

원본 비공개 파일 시스템의 사양

브라우저의 다른 스토리지 메커니즘 (예: localStorage 또는 IndexedDB)과 마찬가지로 원본 비공개 파일 시스템에는 브라우저 할당량 제한이 적용됩니다. 사용자가 모든 인터넷 사용 기록을 삭제하거나 모든 사이트 데이터를 삭제하면 원본 비공개 파일 시스템도 삭제됩니다. navigator.storage.estimate()를 호출하고 결과 응답 객체에서 usage 항목을 확인하여 앱에서 이미 사용 중인 저장용량을 확인합니다. 이는 fileSystem 항목을 구체적으로 살펴보려는 usageDetails 객체의 저장소 메커니즘별로 분류됩니다. 원본 비공개 파일 시스템은 사용자에게 표시되지 않으므로 권한 메시지가 표시되지 않으며 세이프 브라우징 검사도 이루어지지 않습니다.

루트 디렉터리에 액세스 권한 얻기

루트 디렉터리에 액세스하려면 다음 명령어를 실행합니다. 빈 디렉터리 핸들, 특히 FileSystemDirectoryHandle이 생성됩니다.

const opfsRoot = await navigator.storage.getDirectory();
// A FileSystemDirectoryHandle whose type is "directory"
// and whose name is "".
console.log(opfsRoot);

기본 스레드 또는 웹 작업자

원본 비공개 파일 시스템은 기본 스레드 또는 웹 워커에서 사용하는 두 가지 방법으로 사용할 수 있습니다. 웹 작업자는 기본 스레드를 차단할 수 없습니다. 즉, 이 컨텍스트에서 API는 동기식일 수 있으며, 이는 일반적으로 기본 스레드에서 허용되지 않습니다. 동기식 API는 프로미스를 처리할 필요가 없으므로 더 빠를 수 있으며 파일 작업은 일반적으로 WebAssembly에 컴파일할 수 있는 C와 같은 언어에서 동기식입니다.

// This is synchronous C code.
FILE *f;
f = fopen("example.txt", "w+");
fputs("Some text\n", f);
fclose(f);

최대한 빠른 파일 작업이 필요하거나 WebAssembly를 다루는 경우 웹 작업자에서 원본 비공개 파일 시스템 사용으로 건너뛰세요. 그 외의 경우에는 계속 읽어 보세요.

기본 스레드에서 원본 비공개 파일 시스템 사용

새 파일 및 폴더 만들기

루트 폴더가 생성되면 getFileHandle() 메서드와 getDirectoryHandle() 메서드를 각각 사용하여 파일과 폴더를 만듭니다. {create: true}를 전달하면 파일 또는 폴더가 존재하지 않는 경우 생성됩니다. 새로 만든 디렉터리를 시작점으로 사용하여 이러한 함수를 호출하여 파일의 계층 구조를 빌드합니다.

const fileHandle = await opfsRoot
    .getFileHandle('my first file', {create: true});
const directoryHandle = await opfsRoot
    .getDirectoryHandle('my first folder', {create: true});
const nestedFileHandle = await directoryHandle
    .getFileHandle('my first nested file', {create: true});
const nestedDirectoryHandle = await directoryHandle
    .getDirectoryHandle('my first nested folder', {create: true});

이전 코드 샘플의 결과 파일 계층 구조

기존 파일 및 폴더에 액세스

이름을 알고 있는 경우 getFileHandle() 또는 getDirectoryHandle() 메서드를 호출하고 파일 또는 폴더의 이름을 전달하여 이전에 만든 파일 및 폴더에 액세스합니다.

const existingFileHandle = await opfsRoot.getFileHandle('my first file');
const existingDirectoryHandle = await opfsRoot
    .getDirectoryHandle('my first folder');

읽기를 위해 파일 핸들과 연결된 파일 가져오기

FileSystemFileHandle는 파일 시스템의 파일을 나타냅니다. 연결된 File를 가져오려면 getFile() 메서드를 사용합니다. File 객체는 특정 종류의 Blob이며 Blob가 가능한 모든 컨텍스트에서 사용할 수 있습니다. 특히 FileReader, URL.createObjectURL(), createImageBitmap(), XMLHttpRequest.send()BlobsFiles을 모두 허용합니다. 이 경우 FileSystemFileHandle에서 File를 가져오면 데이터가 '해제'되므로 데이터에 액세스하여 사용자에게 표시되는 파일 시스템에서 사용할 수 있습니다.

const file = await fileHandle.getFile();
console.log(await file.text());

스트리밍을 통해 파일에 쓰기

createWritable()를 호출하여 데이터를 파일로 스트리밍합니다. 그러면 이 호출로 FileSystemWritableFileStream를 만든 다음 콘텐츠를 write()할 수 있습니다. 마지막에는 스트림을 close()해야 합니다.

const contents = 'Some text';
// Get a writable stream.
const writable = await fileHandle.createWritable();
// Write the contents of the file to the stream.
await writable.write(contents);
// Close the stream, which persists the contents.
await writable.close();

파일 및 폴더 삭제

파일 또는 디렉터리 핸들의 특정 remove() 메서드를 호출하여 파일 및 폴더를 삭제합니다. 모든 하위 폴더를 포함한 폴더를 삭제하려면 {recursive: true} 옵션을 전달합니다.

await fileHandle.remove();
await directoryHandle.remove({recursive: true});

또는 디렉터리에서 삭제할 파일 또는 폴더의 이름을 알고 있는 경우 removeEntry() 메서드를 사용합니다.

directoryHandle.removeEntry('my first nested file');

파일 및 폴더 이동 및 이름 바꾸기

move() 메서드를 사용하여 파일 및 폴더의 이름을 바꾸고 파일 및 폴더를 이동합니다. 이동과 이름 변경은 함께 또는 개별적으로 수행할 수 있습니다.

// Rename a file.
await fileHandle.move('my first renamed file');
// Move a file to another directory.
await fileHandle.move(nestedDirectoryHandle);
// Move a file to another directory and rename it.
await fileHandle
    .move(nestedDirectoryHandle, 'my first renamed and now nested file');

파일 또는 폴더의 경로 확인

참조 디렉터리를 기준으로 특정 파일이나 폴더가 있는 위치를 알아보려면 resolve() 메서드를 사용하여 FileSystemHandle를 인수로 전달합니다. 원본 비공개 파일 시스템에서 파일 또는 폴더의 전체 경로를 가져오려면 루트 디렉터리를 navigator.storage.getDirectory()를 통해 얻은 참조 디렉터리로 사용합니다.

const relativePath = await opfsRoot.resolve(nestedDirectoryHandle);
// `relativePath` is `['my first folder', 'my first nested folder']`.

두 개의 파일 또는 폴더 핸들이 동일한 파일 또는 폴더를 가리키는지 확인

핸들이 두 개 있는데 동일한 파일이나 폴더를 가리키는지 알 수 없을 때가 있습니다. 이 경우에 해당하는지 확인하려면 isSameEntry() 메서드를 사용하세요.

fileHandle.isSameEntry(nestedFileHandle);
// Returns `false`.

폴더의 콘텐츠 나열

FileSystemDirectoryHandlefor await…of 루프로 반복하는 비동기 반복기입니다. 비동기 반복기로서 entries(), values(), keys() 메서드도 지원하며, 필요한 정보에 따라 선택할 수 있습니다.

for await (let [name, handle] of directoryHandle) {}
for await (let [name, handle] of directoryHandle.entries()) {}
for await (let handle of directoryHandle.values()) {}
for await (let name of directoryHandle.keys()) {}

폴더 및 모든 하위 폴더의 콘텐츠를 재귀적으로 나열

비동기 루프 및 함수를 재귀와 함께 다루는 것은 잘못하기 쉽습니다. 아래 함수는 모든 파일 및 폴더 크기를 포함하여 폴더와 모든 하위 폴더의 콘텐츠를 나열하기 위한 시작 지점 역할을 할 수 있습니다. 파일 크기가 필요하지 않은 경우 directoryEntryPromises.push이라고 표시된 경우 handle.getFile() 프로미스를 푸시하지 않고 handle를 직접 푸시하여 함수를 단순화할 수 있습니다.

  const getDirectoryEntriesRecursive = async (
    directoryHandle,
    relativePath = '.',
  ) => {
    const fileHandles = [];
    const directoryHandles = [];
    const entries = {};
    // Get an iterator of the files and folders in the directory.
    const directoryIterator = directoryHandle.values();
    const directoryEntryPromises = [];
    for await (const handle of directoryIterator) {
      const nestedPath = `${relativePath}/${handle.name}`;
      if (handle.kind === 'file') {
        fileHandles.push({ handle, nestedPath });
        directoryEntryPromises.push(
          handle.getFile().then((file) => {
            return {
              name: handle.name,
              kind: handle.kind,
              size: file.size,
              type: file.type,
              lastModified: file.lastModified,
              relativePath: nestedPath,
              handle
            };
          }),
        );
      } else if (handle.kind === 'directory') {
        directoryHandles.push({ handle, nestedPath });
        directoryEntryPromises.push(
          (async () => {
            return {
              name: handle.name,
              kind: handle.kind,
              relativePath: nestedPath,
              entries:
                  await getDirectoryEntriesRecursive(handle, nestedPath),
              handle,
            };
          })(),
        );
      }
    }
    const directoryEntries = await Promise.all(directoryEntryPromises);
    directoryEntries.forEach((directoryEntry) => {
      entries[directoryEntry.name] = directoryEntry;
    });
    return entries;
  };

Web Worker에서 원본 비공개 파일 시스템 사용

앞서 설명한 것처럼 웹 작업자는 기본 스레드를 차단할 수 없습니다. 이것이 바로 이 컨텍스트에서 동기식 메서드가 허용되는 이유입니다.

동기 액세스 핸들 가져오기

가장 빠른 파일 작업의 진입점은 FileSystemSyncAccessHandle입니다. createSyncAccessHandle()를 호출하여 일반 FileSystemFileHandle에서 가져온 것입니다.

const fileHandle = await opfsRoot
    .getFileHandle('my highspeed file.txt', {create: true});
const syncAccessHandle = await fileHandle.createSyncAccessHandle();

동기식 인플레이스(In-Place) 파일 메서드

동기 액세스 핸들이 있으면 모두 동기식인 빠른 내부 파일 메서드에 액세스할 수 있습니다.

  • getSize(): 파일 크기를 바이트 단위로 반환합니다.
  • write(): 버퍼의 콘텐츠를 파일에 쓰고(선택적으로 지정된 오프셋에서) 작성한 바이트 수를 반환합니다. 반환된 쓰기 바이트 수를 확인하면 호출자가 오류 및 부분 쓰기를 감지하고 처리할 수 있습니다.
  • read(): 선택적으로 지정된 오프셋에서 파일의 콘텐츠를 버퍼로 읽습니다.
  • truncate(): 파일 크기를 지정된 크기로 조정합니다.
  • flush(): 파일의 콘텐츠에 write()를 통해 실행된 모든 수정사항이 포함되도록 합니다.
  • close(): 액세스 핸들을 닫습니다.

다음은 위에서 언급한 모든 메서드를 사용하는 예입니다.

const opfsRoot = await navigator.storage.getDirectory();
const fileHandle = await opfsRoot.getFileHandle('fast', {create: true});
const accessHandle = await fileHandle.createSyncAccessHandle();

const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();

// Initialize this variable for the size of the file.
let size;
// The current size of the file, initially `0`.
size = accessHandle.getSize();
// Encode content to write to the file.
const content = textEncoder.encode('Some text');
// Write the content at the beginning of the file.
accessHandle.write(content, {at: size});
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `9` (the length of "Some text").
size = accessHandle.getSize();

// Encode more content to write to the file.
const moreContent = textEncoder.encode('More content');
// Write the content at the end of the file.
accessHandle.write(moreContent, {at: size});
// Flush the changes.
accessHandle.flush();
// The current size of the file, now `21` (the length of
// "Some textMore content").
size = accessHandle.getSize();

// Prepare a data view of the length of the file.
const dataView = new DataView(new ArrayBuffer(size));

// Read the entire file into the data view.
accessHandle.read(dataView);
// Logs `"Some textMore content"`.
console.log(textDecoder.decode(dataView));

// Read starting at offset 9 into the data view.
accessHandle.read(dataView, {at: 9});
// Logs `"More content"`.
console.log(textDecoder.decode(dataView));

// Truncate the file after 4 bytes.
accessHandle.truncate(4);

원본 비공개 파일 시스템에서 사용자에게 표시되는 파일 시스템으로 파일 복사

위에서 언급했듯이 원본 비공개 파일 시스템에서 사용자에게 표시되는 파일 시스템으로 파일을 이동할 수 없지만 파일을 복사할 수는 있습니다. showSaveFilePicker()는 기본 스레드에서만 노출되고 작업자 스레드에는 노출되지 않으므로 여기서 코드를 실행해야 합니다.

// On the main thread, not in the Worker. This assumes
// `fileHandle` is the `FileSystemFileHandle` you obtained
// the `FileSystemSyncAccessHandle` from in the Worker
// thread. Be sure to close the file in the Worker thread first.
const fileHandle = await opfsRoot.getFileHandle('fast');
try {
  // Obtain a file handle to a new file in the user-visible file system
  // with the same name as the file in the origin private file system.
  const saveHandle = await showSaveFilePicker({
    suggestedName: fileHandle.name || ''
  });
  const writable = await saveHandle.createWritable();
  await writable.write(await fileHandle.getFile());
  await writable.close();
} catch (err) {
  console.error(err.name, err.message);
}

원본 비공개 파일 시스템 디버그

기본 제공되는 DevTools 지원이 추가될 때까지 (crbug/1284595 참고) OPFS Explorer Chrome 확장 프로그램을 사용하여 원본 비공개 파일 시스템을 디버그합니다. 위의 새 파일 및 폴더 만들기 섹션에 있는 스크린샷은 확장 프로그램에서 직접 가져온 것입니다.

Chrome 웹 스토어의 OPFS Explorer Chrome DevTools 확장 프로그램

확장 프로그램을 설치한 후 Chrome DevTools를 열고 OPFS Explorer 탭을 선택하면 파일 계층 구조를 검사할 수 있습니다. 파일 이름을 클릭하여 원본 비공개 파일 시스템의 파일을 사용자에게 표시되는 파일 시스템으로 저장하고 휴지통 아이콘을 클릭하여 파일과 폴더를 삭제합니다.

데모

OPFS 탐색기 확장 프로그램을 설치한 경우 WebAssembly에 컴파일된 SQLite 데이터베이스의 백엔드로 사용하는 원본 비공개 파일 시스템을 데모에서 확인하세요. Glitch의 소스 코드를 확인하세요. 아래에 삽입된 버전은 (iframe이 교차 출처이기 때문에) 원본 비공개 파일 시스템 백엔드를 사용하지 않지만 별도의 탭에서 데모를 열면 사용합니다.

결론

WhatWG에 명시된 대로 원본 개인 파일 시스템은 우리가 웹에서 파일을 사용하고 상호작용하는 방식을 형성해 왔습니다. 사용자가 볼 수 있는 파일 시스템으로는 실현할 수 없었던 새로운 사용 사례가 가능해졌습니다. Apple, Mozilla, Google을 비롯한 모든 주요 브라우저 공급업체가 참여하고 있으며 공동의 비전을 공유합니다. 원본 비공개 파일 시스템의 개발은 많은 공동의 노력이 필요하며, 이를 발전시키기 위해서는 개발자와 사용자의 의견이 필수적입니다. Google이 표준을 지속적으로 개선하고 개선함에 따라 문제 또는 pull 요청 형태로 whatwg/fs 저장소에 대한 의견을 언제든지 환영합니다.

감사의 말

Austin Sully, Etienne Noèl, Rachel Andrew가 작성한 도움말입니다. UnsplashChristina Rumpf의 히어로 이미지입니다.