파일 시스템 표준은 페이지의 출처에 비공개이고 사용자에게 표시되지 않으며 성능에 매우 최적화된 특수한 종류의 파일에 대한 선택적 액세스를 제공하는 스토리지 엔드포인트로 출처 비공개 파일 시스템 (OPFS)을 도입합니다.
브라우저 지원
출처 비공개 파일 시스템은 최신 브라우저에서 지원되며 웹 하이퍼텍스트 애플리케이션 기술 작업 그룹 (WHATWG)에 의해 파일 시스템 라이브 표준에서 표준화됩니다.
동기
컴퓨터의 파일을 생각할 때 운영체제의 파일 탐색기로 탐색할 수 있는 폴더에 정리된 파일의 파일 계층 구조를 생각할 것입니다. 예를 들어 Windows에서 Tom이라는 사용자의 할 일 목록은 C:\Users\Tom\Documents\ToDo.txt에 있을 수 있습니다. 이 예에서 ToDo.txt는 파일 이름이고 Users, Tom, Documents는 폴더 이름입니다. Windows의 `C:` 는 드라이브의 루트 디렉터리를 나타냅니다.
웹에서 파일로 작업하는 일반적인 방법
웹 애플리케이션에서 할 일 목록을 수정하는 일반적인 흐름은 다음과 같습니다.
- 사용자가 파일을 서버에 업로드하거나 클라이언트에서
<input type="file">로 엽니다. - 사용자가 변경한 다음 다운로드하는 삽입된
<a download="ToDo.txt>결과 파일을 JavaScript로 프로그래매틱 방식으로click()합니다. - 폴더를 열려면
<input type="file" webkitdirectory>의 특수 속성을 사용합니다. 이 속성은 독점적인 이름에도 불구하고 사실상 보편적인 브라우저 지원을 제공합니다.
웹에서 파일로 작업하는 최신 방법
이 흐름은 사용자가 파일을 수정하는 방식을 나타내지 않으며 사용자가 입력 파일의 다운로드된 사본 을 갖게 됩니다. 따라서 File System Access API는 다음과 같은 세 가지 선택 도구 메서드를 도입했습니다.
다음과 같은 흐름을 사용 설정합니다.
showOpenFilePicker()로ToDo.txt를 열고FileSystemFileHandle객체를 가져옵니다.FileSystemFileHandle객체에서 파일 핸들의getFile()메서드를 호출하여File을 가져옵니다.- 파일을 수정한 다음 핸들에서
requestPermission({mode: 'readwrite'})를 호출합니다. - 사용자가 권한 요청을 수락하면 변경사항을 원래 파일에 다시 저장합니다.
- 또는
showSaveFilePicker()를 호출하고 사용자가 새 파일을 선택하도록 합니다. (사용자가 이전에 열었던 파일을 선택하면 콘텐츠가 덮어쓰여집니다.) 저장을 반복하려면 파일 핸들을 유지하여 파일 저장 대화상자를 다시 표시하지 않아도 됩니다.
웹에서 파일로 작업할 때의 제한사항
이러한 메서드로 액세스할 수 있는 파일과 폴더는 사용자에게 표시되는 파일 시스템이라고 할 수 있는 곳에 있습니다. 웹에서 저장된 파일과 특히 실행 파일은 웹의 표시로 표시되므로, 잠재적으로 위험한 파일이 실행되기 전에 운영체제에서 표시할 수 있는 추가 경고가 있습니다. 추가 보안 기능으로 웹에서 가져온 파일은 간소화를 위해 이 문서의 컨텍스트에서 클라우드 기반 바이러스 검사로 생각할 수 있는 세이프 브라우징으로도 보호됩니다. File System Access API를 사용하여 파일에 데이터를 쓸 때 쓰기는 제자리에서 이루어지지 않고 임시 파일을 사용합니다. 파일 자체는 이러한 모든 보안 검사를 통과하지 않는 한 수정되지 않습니다.
이 작업은 가능한 경우
적용된 개선사항에도 불구하고 파일 작업을 비교적 느리게 만듭니다. 예를 들어, macOS에서 말이죠.
하지만 모든 write()
호출은 자체 포함되어 있으므로 내부적으로 파일을 열고 지정된
오프셋을 찾은 다음 데이터를 씁니다.
처리의 기반이 되는 파일
동시에 파일은 데이터를 기록하는 데 유용한 방법입니다. 예를 들어 SQLite는 전체 데이터베이스를 단일 파일에 저장합니다. 또 다른 예는 밉맵입니다. 이미지 처리에서 사용됩니다. 밉맵은 미리 계산되고 최적화된 이미지 시퀀스이며 각 이미지는 이전 이미지의 해상도가 점진적으로 낮은 표현이므로 확대/축소와 같은 많은 작업을 더 빠르게 만듭니다. 그렇다면 웹 애플리케이션은 웹 기반 파일 처리의 성능 비용 없이 파일의 이점을 어떻게 얻을 수 있을까요? 답은 출처 비공개 파일 시스템 입니다.
사용자에게 표시되는 파일 시스템과 출처 비공개 파일 시스템
운영체제의 파일 탐색기를 사용하여 탐색하고 읽기, 쓰기, 이동, 이름 바꾸기를 할 수 있는 파일과 폴더가 있는 사용자에게 표시되는 파일 시스템과 달리 출처 비공개 파일 시스템은 사용자에게 표시되지 않습니다. 출처 비공개 파일 시스템의 파일과 폴더는 이름에서 알 수 있듯이 비공개이며 더
구체적으로는 사이트의 출처
에 비공개입니다. DevTools 콘솔에
location.origin
을 입력하여 페이지의 출처를 확인합니다. 예를 들어
https://developer.chrome.com/articles/ 페이지의 출처는 https://developer.chrome.com입니다.
출처 이론에 관한 자세한 내용은
'동일 사이트' 및 '동일 출처' 이해를 참고하세요.
동일한 출처를 공유하는 모든 페이지는 동일한 출처 비공개 파일 시스템 데이터를 볼 수 있으므로 https://developer.chrome.com/docs/extensions/mv3/getstarted/extensions-101/은 이전 예와 동일한 세부정보를 볼 수 있습니다. 각 출처에는 자체 독립적인 출처 비공개 파일 시스템이 있으므로 https://developer.chrome.com의 출처 비공개 파일 시스템은 예를 들어 https://web.dev의 출처 비공개 파일 시스템과 완전히 다릅니다. Windows에서 사용자에게 표시되는 파일 시스템의 루트 디렉터리는 C:\\입니다.
출처 비공개 파일 시스템의 동등한 항목은 비동기 메서드
navigator.storage.getDirectory()를 호출하여 액세스하는 출처당 처음에는 비어 있는 루트
디렉터리입니다.
사용자에게 표시되는 파일 시스템과 출처 비공개 파일 시스템을 비교하려면 다음 다이어그램을 참고하세요. 다이어그램은 루트 디렉터리를 제외하고는 모든 것이 개념적으로 동일하며 데이터 및 스토리지 요구사항에 따라 필요한 대로 구성하고 정렬할 수 있는 파일 및 폴더 계층 구조가 있음을 보여줍니다.
navigator.storage.getDirectory 호출입니다.출처 비공개 파일 시스템의 세부정보
브라우저의 다른 스토리지 메커니즘 (예: localStorage 또는 IndexedDB)과 마찬가지로 출처 비공개 파일 시스템에는 브라우저 할당량 제한이 적용됩니다. 사용자가 모든 방문 기록 데이터 또는 모든 사이트 데이터를 삭제하면, 출처 비공개 파일 시스템도 삭제됩니다.
navigator.storage.estimate()를 호출하고 결과 응답 객체에서
usage 항목을 확인하여 앱에서 이미 사용 중인 스토리지 용량을 확인합니다. 이는
usageDetails 객체의 스토리지 메커니즘별로 분류되며 여기에서 특히 fileSystem 항목을 확인해야 합니다. 출처 비공개 파일 시스템은 사용자에게 표시되지 않으므로 권한 프롬프트와 세이프 브라우징 검사가 없습니다.
루트 디렉터리에 액세스
루트 디렉터리에 액세스하려면 다음 명령어를 실행합니다. 결과적으로 빈 디렉터리 핸들, 더 구체적으로는
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()
는 Blobs와 Files를 모두 허용합니다. File에서
FileSystemFileHandle을 가져오면 데이터가 '해제'되므로 데이터에 액세스하고 사용자에게 표시되는 파일 시스템에서 사용할 수 있도록 할 수 있습니다.
const file = await fileHandle.getFile();
console.log(await file.text());
스트리밍으로 파일에 쓰기
createWritable()
을 호출하여 데이터를 파일로 스트리밍합니다. 그러면 콘텐츠를 write()
하는 FileSystemWritableFileStream
이 생성됩니다. 마지막으로 스트림을
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`.
폴더의 콘텐츠 나열
FileSystemDirectoryHandle은
비동기 반복기
입니다. 이 반복기는
for 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;
};
웹 워커에서 출처 비공개 파일 시스템 사용
앞서 설명한 것처럼 웹 워커는 기본 스레드를 차단할 수 없으므로 이 컨텍스트에서 동기식 메서드가 허용됩니다.
동기식 액세스 핸들 가져오기
가능한 가장 빠른 파일 작업의 진입점은
FileSystemSyncAccessHandle,
일반
FileSystemFileHandle
에서 createSyncAccessHandle()을 호출하여 가져온 것입니다.
const fileHandle = await opfsRoot
.getFileHandle('my highspeed file.txt', {create: true});
const syncAccessHandle = await fileHandle.createSyncAccessHandle();
동기식 제자리 파일 메서드
동기식 액세스 핸들이 있으면 모두 동기식인 빠른 제자리 파일 메서드에 액세스할 수 있습니다.
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);
read()write()ArrayBufferArrayBufferViewDataView ArrayBuffer의 콘텐츠를 직접 조작할 수는 없습니다. 대신 특정 형식으로 버퍼를 나타내는 유형화된 배열 객체 중 하나를 만들고 이를 사용하여 버퍼의 콘텐츠를 읽고 씁니다.Int8ArrayDataView
출처 비공개 파일 시스템에서 사용자에게 표시되는 파일 시스템으로 파일 복사
위에서 언급한 것처럼 출처 비공개 파일 시스템에서 사용자에게 표시되는 파일 시스템으로 파일을 이동할 수는 없지만 파일을 복사할 수는 있습니다. 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 DevTools를 열고 OPFS Explorer 탭을 선택하면 파일 계층 구조를 검사할 수 있습니다. 파일 이름을 클릭하여 출처 비공개 파일 시스템에서 사용자에게 표시되는 파일 시스템으로 파일을 저장하고 휴지통 아이콘을 클릭하여 파일과 폴더를 삭제합니다.
데모
WebAssembly로 컴파일된 SQLite 데이터베이스의 백엔드로 사용하는 데모에서 출처 비공개 파일 시스템이 작동하는 모습을 확인하세요 (OPFS Explorer 확장 프로그램을 설치한 경우). GitHub에서 소스 코드를 확인하세요. 삽입된 버전은 iframe이 교차 출처이므로 출처 비공개 파일 시스템 백엔드를 사용하지 않지만 별도의 탭에서 데모를 열면 사용합니다.
결론
WHATWG에서 지정한 출처 비공개 파일 시스템은 웹에서 파일을 사용하고 상호작용하는 방식을 형성했습니다. 사용자에게 표시되는 파일 시스템으로는 불가능했던 새로운 사용 사례를 지원했습니다. 모든 주요 브라우저 공급업체(Apple, Mozilla, Google)가 참여하고 공동 비전을 공유합니다. 출처 비공개 파일 시스템의 개발은 매우 협력적인 노력이며 개발자와 사용자의 의견은 진행에 필수적입니다.
표준을 계속 개선하고 개선함에 따라 문제 또는 Pull 요청 형식의 whatwg/fs 저장소에 대한 의견을 보내주시면 감사하겠습니다.
관련 링크
감사의 말씀
이 문서는 Austin Sully, Etienne Noël, and Rachel Andrew가 검토했습니다.