브라우저는 오랫동안 파일과 디렉터리를 처리할 수 있었습니다. 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를 사용하면 열기와 저장 작업을 훨씬 더 간단하게 처리할 수 있습니다. 또한 실제 저장을 지원합니다. 즉, 파일을 저장할 위치를 선택할 뿐만 아니라 기존 파일을 덮어쓸 수도 있습니다.
파일 열기
파일 시스템 액세스 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를 점진적 개선으로 보는 이유입니다. 따라서 브라우저에서 지원하는 경우 이를 사용하고 지원되지 않는 JavaScript 코드를 불필요하게 다운로드하여 사용자를 불편하게 하지 않으면서 기존 접근 방식을 사용하는 것이 좋습니다. browser-fs-access 라이브러리가 이 문제에 대한 해결책입니다.
디자인 철학
File System Access API는 향후 변경될 가능성이 있으므로 browser-fs-access API는 이를 모델로 하지 않습니다.
즉, 이 라이브러리는 폴리필이 아니라 포니필입니다.
앱을 최대한 작게 유지하는 데 필요한 기능만 (정적 또는 동적으로) 독점적으로 가져올 수 있습니다.
사용 가능한 메서드는 적절한 이름이 지정된 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를 지원하지 않으므로 다운로드)한 다음 휴대전화에서 전송한 후 데스크톱에서 파일을 열고 파일을 수정한 후 변경사항으로 덮어쓰거나 새 파일로 저장할 수 있습니다.
실제 코드 샘플
아래는 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) {}
) 저장 버튼 외에 다른 이름으로 저장 버튼을 표시할 수 있습니다.
아래 스크린샷은 iPhone과 Chrome 데스크톱에서 실행되는 Excalidraw의 반응형 기본 앱 툴바의 차이를 보여줍니다.
iPhone에서는 다른 이름으로 저장 버튼이 누락된 것을 볼 수 있습니다.
결론
시스템 파일 작업은 기술적으로 모든 최신 브라우저에서 작동합니다. File System Access API를 지원하는 브라우저에서는 파일의 다운로드뿐만 아니라 실제 저장 및 덮어쓰기를 허용하고 사용자가 원하는 위치에 새 파일을 만들 수 있도록 허용하여 환경을 개선할 수 있습니다. 이때 File System Access API를 지원하지 않는 브라우저에서도 계속 작동합니다. browser-fs-access를 사용하면 점진적 개선의 미묘한 차이를 처리하고 코드를 최대한 간단하게 만들어 개발자의 작업을 간소화할 수 있습니다.
감사의 말씀
이 도움말은 조 미들리와 케이스 바스케스가 검토했습니다. 프로젝트에 참여하고 내 풀 리퀘스트를 검토해 주신 Excalidraw 참여자 여러분께 감사드립니다. Unsplash의 Ilya Pavlov님 제공 히어로 이미지