브라우저-fs-access 라이브러리로 파일과 디렉터리 읽기 및 쓰기

브라우저는 오랫동안 파일과 디렉터리를 처리할 수 있었습니다. File API는 웹 애플리케이션에서 파일 객체를 나타내고 프로그래매틱 방식으로 파일을 선택하고 데이터에 액세스하기 위한 기능을 제공합니다. 하지만 가까이에서 보는 순간 반짝이는 모든 것이 금은 아닙니다.

파일을 처리하는 전통적인 방법

파일 열기

개발자는 <input type="file"> 요소를 통해 파일을 열고 읽을 수 있습니다. 가장 간단한 형태의 파일을 여는 것은 아래의 코드 샘플과 같습니다. input 객체는 FileList를 제공합니다. 아래 경우에는 File 하나로 구성됩니다. File는 특정 종류의 Blob이며 Blob에서 사용할 수 있는 모든 컨텍스트에서 사용할 수 있습니다.

const openFile = async () => {
  return new Promise((resolve) => {
    const input = document.createElement('input');
    input.type = 'file';
    input.addEventListener('change', () => {
      resolve(input.files[0]);
    });
    input.click();
  });
};

디렉터리 열기

폴더 (또는 디렉터리)를 여는 경우 <input webkitdirectory> 속성을 설정할 수 있습니다. 그 외의 모든 작업은 위와 동일하게 작동합니다. 공급업체가 접두사로 지정된 이름에도 불구하고 webkitdirectory는 Chromium과 WebKit 브라우저뿐만 아니라 기존 EdgeHTML 기반 Edge와 Firefox에서도 사용할 수 있습니다.

파일 저장 (다운로드 아님)

파일을 저장하는 경우 일반적으로 파일 다운로드로 제한되며 이는 <a download> 속성 덕분에 작동합니다. Blob이 주어지면 앵커의 href 속성을 URL.createObjectURL() 메서드에서 가져올 수 있는 blob: URL로 설정할 수 있습니다.

const saveFile = async (blob) => {
  const a = document.createElement('a');
  a.download = 'my-file.txt';
  a.href = URL.createObjectURL(blob);
  a.addEventListener('click', (e) => {
    setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
  });
  a.click();
};

문제

다운로드 방식의 큰 단점은 기존의 열기, 수정, 저장 흐름을 만들 수 있는 방법이 없다는 것입니다. 즉, 원본 파일을 덮어쓸 방법이 없습니다. 대신 '저장'할 때마다 운영체제의 기본 다운로드 폴더에 원본 파일의 새 사본이 생성됩니다.

File System Access API입니다.

File System Access API를 사용하면 열기 및 저장 작업이 모두 훨씬 간단해집니다. 또한 실제 저장을 사용 설정합니다. 즉, 파일을 저장할 위치를 선택할 수 있을 뿐만 아니라 기존 파일을 덮어쓸 수도 있습니다.

파일 열기

File System Access API를 사용하면 window.showOpenFilePicker() 메서드를 한 번만 호출하면 파일을 열 수 있습니다. 이 호출은 파일 핸들을 반환하며, getFile() 메서드를 통해 실제 File를 가져올 수 있습니다.

const openFile = async () => {
  try {
    // Always returns an array.
    const [handle] = await window.showOpenFilePicker();
    return handle.getFile();
  } catch (err) {
    console.error(err.name, err.message);
  }
};

디렉터리 열기

파일 대화상자에서 디렉터리를 선택할 수 있도록 하는 window.showDirectoryPicker()를 호출하여 디렉터리를 엽니다.

파일 저장 중

파일을 저장하는 방법도 간단합니다. 파일 핸들에서 createWritable()를 통해 쓰기 가능한 스트림을 만든 후 스트림의 write() 메서드를 호출하여 Blob 데이터를 작성하고 마지막으로 close() 메서드를 호출하여 스트림을 닫습니다.

const saveFile = async (blob) => {
  try {
    const handle = await window.showSaveFilePicker({
      types: [{
        accept: {
          // Omitted
        },
      }],
    });
    const writable = await handle.createWritable();
    await writable.write(blob);
    await writable.close();
    return handle;
  } catch (err) {
    console.error(err.name, err.message);
  }
};

browser-fs-access 소개

File System Access API는 완벽하지만 아직 널리 사용되지는 않습니다.

File System Access API의 브라우저 지원 표 모든 브라우저는 &#39;지원되지 않음&#39; 또는 &#39;플래그 뒤쪽&#39;으로 표시됩니다.
File System Access API의 브라우저 지원 표입니다. (출처)

이러한 이유로 File System Access API를 점진적 개선이라고 생각합니다. 따라서 브라우저에서 지원하는 경우에만 사용하고 지원되지 않는 경우에는 기존의 접근 방식을 사용하는 동시에 지원되지 않는 JavaScript 코드를 불필요하게 다운로드하여 사용자를 처벌해서는 안 됩니다. browser-fs-access 라이브러리가 이 문제에 대한 해답입니다.

디자인 철학

File System Access API는 향후 변경될 가능성이 여전히 크므로 browser-fs-access API는 이를 모델링하지 않습니다. 즉, 라이브러리는 polyfill이 아니라 포니필입니다. 앱을 가능한 한 작게 유지하는 데 필요한 모든 기능을 정적 또는 동적으로 독점적으로 가져올 수 있습니다. 사용 가능한 메서드는 적절하게 명명된 fileOpen(), directoryOpen(), fileSave()입니다. 내부적으로 라이브러리는 File System Access API 지원 여부를 감지한 다음 상응하는 코드 경로를 가져옵니다.

browser-fs-access 라이브러리 사용

세 가지 방법은 직관적으로 사용할 수 있습니다. 앱에서 허용되는 mimeTypes 또는 extensions 파일을 지정하고 multiple 플래그를 설정하여 여러 파일이나 디렉터리 선택을 허용하거나 허용하지 않을 수 있습니다. 자세한 내용은 browser-fs-access API 문서를 참고하세요. 아래의 코드 샘플은 이미지 파일을 열고 저장하는 방법을 보여줍니다.

// The imported methods will use the File
// System Access API or a fallback implementation.
import {
  fileOpen,
  directoryOpen,
  fileSave,
} from 'https://unpkg.com/browser-fs-access';

(async () => {
  // Open an image file.
  const blob = await fileOpen({
    mimeTypes: ['image/*'],
  });

  // Open multiple image files.
  const blobs = await fileOpen({
    mimeTypes: ['image/*'],
    multiple: true,
  });

  // Open all files in a directory,
  // recursively including subdirectories.
  const blobsInDirectory = await directoryOpen({
    recursive: true
  });

  // Save a file.
  await fileSave(blob, {
    fileName: 'Untitled.png',
  });
})();

데모

Glitch에 관한 데모에서 위의 코드가 실제로 작동하는 모습을 확인할 수 있습니다. 소스 코드도 이곳에서 확인할 수 있습니다. 보안상의 이유로 교차 출처 하위 프레임에 파일 선택 도구를 표시할 수 없으므로 데모를 이 문서에 삽입할 수 없습니다.

실제 브라우저-fs-access 라이브러리

시간이 있을 때는 손으로 그린 느낌으로 다이어그램을 쉽게 스케치할 수 있는 화이트보드 도구인 Excalidraw라는 설치 가능한 PWA를 제공합니다. 반응 속도가 높으며 소형 휴대전화부터 대형 화면의 컴퓨터에 이르기까지 다양한 기기에서 원활하게 작동합니다. 즉, File System Access API 지원 여부와 관계없이 다양한 모든 플랫폼의 파일을 처리해야 합니다. 따라서 browser-fs-access 라이브러리에 적합합니다.

예를 들어 iPhone에서 그림을 시작한 다음 iPhone 다운로드 폴더에 저장 (엄밀히 말하면 Safari는 File System Access API를 지원하지 않으므로 다운로드)하고 데스크톱에서 파일을 열고 (휴대전화에서 전송한 후) 파일을 수정하고 변경사항을 덮어쓰거나 새 파일로 저장할 수 있습니다.

iPhone에 표시된 Excalidraw 그림
File System Access API가 지원되지 않지만 다운로드 폴더에 파일을 저장 (다운로드)할 수 있는 iPhone에서 Excalidraw 그리기
데스크톱의 Chrome에서 수정된 Excalidraw 그림
File System Access API가 지원되므로 API를 통해 파일에 액세스할 수 있는 데스크톱에서 Excalidraw 그리기 열기 및 수정
원본 파일을 수정하여 수정합니다.
원본 Excalidraw 그리기 파일의 수정사항을 원본 파일로 덮어씁니다. 브라우저에 문제가 없는지 묻는 대화상자가 표시됩니다.
새 Excalidraw 그리기 파일에 수정사항을 저장합니다.
새 Excalidraw 파일에 수정사항을 저장합니다. 원본 파일은 그대로 유지됩니다.

실제 코드 샘플

아래에서는 Excalidraw에서 사용되는 browser-fs-access의 실제 예를 볼 수 있습니다. 이 발췌 부분은 /src/data/json.ts에서 가져왔습니다. saveAsJSON() 메서드가 파일 핸들 또는 null를 브라우저-fs-access의 fileSave() 메서드에 전달하여 핸들이 제공되면 덮어쓰고 그렇지 않으면 새 파일에 저장하는 방법이 특히 중요합니다.

export const saveAsJSON = async (
  elements: readonly ExcalidrawElement[],
  appState: AppState,
  fileHandle: any,
) => {
  const serialized = serializeAsJSON(elements, appState);
  const blob = new Blob([serialized], {
    type: "application/json",
  });
  const name = `${appState.name}.excalidraw`;
  (window as any).handle = await fileSave(
    blob,
    {
      fileName: name,
      description: "Excalidraw file",
      extensions: ["excalidraw"],
    },
    fileHandle || null,
  );
};

export const loadFromJSON = async () => {
  const blob = await fileOpen({
    description: "Excalidraw files",
    extensions: ["json", "excalidraw"],
    mimeTypes: ["application/json"],
  });
  return loadFromBlob(blob);
};

UI 고려사항

Excalidraw 또는 앱에서 UI가 브라우저의 지원 상황에 맞게 조정되어야 합니다. File System Access API (if ('showOpenFilePicker' in window) {})가 지원되는 경우 Save 버튼 외에 Save As 버튼을 표시할 수 있습니다. 아래 스크린샷은 iPhone과 Chrome 데스크톱에서 Excalidraw의 반응형 기본 앱 툴바가 어떻게 다른지 보여줍니다. iPhone에서는 다른 이름으로 저장 버튼이 표시되지 않습니다.

&#39;저장&#39; 버튼만 있는 iPhone의 Excalidraw 앱 툴바
저장 버튼만 있는 Excalidraw 앱 툴바
&#39;저장&#39; 및 &#39;다른 이름으로 저장&#39; 버튼이 있는 Chrome 데스크톱의 Excalidraw 앱 툴바
저장 및 포커스가 맞춰진 다른 이름으로 저장 버튼이 있는 Chrome의 Excalidraw 앱 툴바

결론

시스템 파일로 작업하는 것은 모든 최신 브라우저에서 기술적으로 작동합니다. File System Access API를 지원하는 브라우저에서는 파일을 실제 저장 및 덮어쓰기 (다운로드뿐 아니라)할 수 있고, File System Access API를 지원하지 않는 브라우저에서도 기능을 유지하면서 사용자가 원하는 곳에 새 파일을 만들 수 있도록 하여 환경을 개선할 수 있습니다. browser-fs-access는 점진적 향상의 미묘한 부분을 다루고 코드를 최대한 단순하게 만들어 더욱 편리한 생활을 선사합니다.

감사의 말

이 문서는 Joe MedleyKayce Basques가 검토했습니다. 프로젝트 작업과 제 pull 요청을 검토해 주신 Excalidraw 참여자께 감사드립니다. 히어로 이미지(일리야 파블로프, Unsplash)