브라우저-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 속성을blob: URL.createObjectURL() 메서드를 사용하여 축소하도록 요청합니다.

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가 완벽한 만큼이나 아직 널리 사용되지는 않습니다.

<ph type="x-smartling-placeholder">
</ph> File System Access API에 대한 브라우저 지원 표입니다. 모든 브라우저가 &#39;지원되지 않음&#39;으로 표시됨 &#39;깃발 뒤에 있는&#39; <ph type="x-smartling-placeholder">
</ph> File System Access API에 대한 브라우저 지원 표입니다. (출처)

이것이 File System Access API가 점진적 개선이라고 생각하는 이유입니다. 따라서 브라우저가 지원할 때 사용하고 싶습니다. 그렇지 않다면 기존의 접근 방식을 사용합니다. 지원되지 않는 JavaScript 코드를 불필요하게 다운로드하여 사용자를 처벌하는 일은 없습니다. browser-fs-access 이 도전과제에 대한 해답이 될 것입니다.

디자인 철학

File System Access API는 향후 변경될 가능성이 높으므로 browser-fs-access API는 모델링되지 않습니다. 즉, 라이브러리는 폴리필이 아닙니다. ponyfill이 아닙니다. 앱을 최대한 작게 유지하는 데 필요한 기능은 무엇이든 정적 또는 동적으로 가져올 수 있습니다. 사용 가능한 메서드는 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의 데모에서 실제로 작동하는 것을 확인할 수 있습니다. 소스 코드도 여기에서 제공됩니다. 보안상의 이유로 교차 출처 서브 프레임은 파일 선택기를 표시할 수 없습니다. 이 도움말에는 데모를 삽입할 수 없습니다.

실제의 browser-fs-access 라이브러리

여가 시간에는 설치 가능한 PWA Excalidraw를 사용하면 손으로 그린 느낌으로 다이어그램을 쉽게 스케치할 수 있는 화이트보드 도구입니다. 이 앱은 응답성이 뛰어나며 작은 휴대전화에서부터 큰 화면이 있는 컴퓨터에 이르기까지 다양한 기기에서 잘 작동합니다. 즉, 다양한 플랫폼의 파일을 처리해야 합니다. File System Access API 지원 여부 따라서 browser-fs-access 라이브러리에 사용하기 좋습니다.

예를 들어 iPhone에서 그림을 그리고 저장합니다 (엄밀히 말하면 다운로드는 Safari가 File System Access API를 지원하지 않기 때문입니다). 파일을 iPhone 다운로드 폴더에 저장하고 데스크톱에서 파일을 열고 (내 휴대폰에서 파일을 전송한 후) 파일을 수정하고, 내가 변경한 내용을 덮어쓰거나, 새 파일로 저장할 수도 있습니다.

<ph type="x-smartling-placeholder">
</ph> iPhone에 그려진 엑스칼리드로(Excalidraw)
File System Access API가 지원되지 않지만 다운로드 폴더에 파일을 저장 (다운로드)할 수 있는 iPhone에서 Excalidraw 드로잉 시작
를 통해 개인정보처리방침을 정의할 수 있습니다. <ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">데스크톱의 Chrome에서 수정된 Excalidraw 그림</ph>
File System Access API가 지원되어 API를 통해 파일에 액세스할 수 있는 데스크톱에서 Excalidraw 그리기 열기 및 수정
를 통해 개인정보처리방침을 정의할 수 있습니다. <ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">원본 파일을 수정사항으로 덮어씁니다.</ph>
원본 Excalidraw 그리기 파일의 수정 사항을 원본 파일에 덮어씁니다. 브라우저에 문제가 없는지 묻는 대화상자가 표시됩니다.
를 통해 개인정보처리방침을 정의할 수 있습니다. <ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">수정사항을 새 Excalidraw 드로잉 파일에 저장</ph>
수정사항을 새 Excalidraw 파일에 저장 원본 파일은 변경되지 않습니다.

실제 코드 샘플

아래에서는 Excalidraw에서 사용되는 browser-fs-access의 실제 예를 볼 수 있습니다. 이 발췌부분은 /src/data/json.ts saveAsJSON() 메서드가 파일 핸들 또는 null를 browser-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에서 다른 이름으로 저장 버튼이 보이지 않습니다.

<ph type="x-smartling-placeholder">
</ph> iPhone의 Excalidraw 앱 툴바와 &#39;Save&#39; 버튼을 클릭합니다.
저장 버튼만 있는 iPhone의 Excalidraw 앱 툴바
를 통해 개인정보처리방침을 정의할 수 있습니다. <ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder">&#39;저장&#39;이 표시된 Chrome 데스크톱의 Excalidraw 앱 툴바 &#39;다른 이름으로 저장&#39; 버튼을 버튼을 클릭합니다.</ph>
저장과 포커스가 맞춰진 다른 이름으로 저장 버튼이 있는 Chrome의 Excalidraw 앱 툴바

결론

시스템 파일 작업은 모든 최신 브라우저에서 기술적으로 작동합니다. File System Access API를 지원하는 브라우저에서는 다운로드뿐만 아니라 파일의 실제 저장 및 덮어쓰기를 지원합니다. 사용자가 어디서나 새 파일을 만들 수 있도록 하여 File System Access API를 지원하지 않는 브라우저에서는 계속 작동합니다. browser-fs-access를 사용하면 편리함 점진적 개선의 미묘한 부분을 처리하고 코드를 가능한 한 단순하게 만들면 됩니다.

감사의 말씀

이 도움말은 Joe Medley가 검토했으며 케이스 바스케스 Excalidraw에 참여해 주신 분들께 감사드립니다. 저의 pull 요청을 검토해 주셔서 감사합니다. 히어로 이미지 제공 일리야 파블로프(Unsplash).