1. 목표
이 명세서는 웹 브라우저가 스크립트에 제공하는 파일 및 디렉터리 계층 구조를 페이지에 드래그 앤 드롭하거나 폼 요소를 통해 선택하거나, 그에 준하는 사용자 동작을 수행할 때 제공되는 타입과 동작을 문서화합니다.
이는 샌드박스 파일 시스템 환경에서 유사한 타입과 파일 및 디렉터리 생성/수정 동작을 정의하지만, 웹 브라우저에서 널리 채택되지 않은 [file-system-api]의 초기 초안에 크게 기반하고 있습니다.
note: 이 문서에서 설명하는 API는 최초로 Google Chrome에 구현되었습니다. 현재(이 시점에서: Edge, Firefox, Safari) 다른 브라우저들도 Chrome의 일부 API와 동작을 부분적으로 지원하기 시작했습니다. 이 문서의 목적은 구현체들이 상호 운용될 수 있도록 공통 부분을 명세하는 데 있습니다.
2. 개념
2.1. 이름과 경로
name은 다음을 만족하는 문자열입니다:
-
'/' (U+002F SOLIDUS)를 포함하지 않음
-
NUL (U+0000)을 포함하지 않음
-
'\' (U+005C REVERSE SOLIDUS)를 포함하지 않음
-
'.' (U+002E FULL STOP)이 아님
-
'..' (U+002E FULL STOP, U+002E FULL STOP)이 아님
path segment는 name, '.' (U+002E FULL STOP) 또는 '..' (U+002E FULL STOP, U+002E FULL STOP)입니다.
relative path는 하나 이상의 path segment가 '/' (U+002F SOLIDUS)로 연결된 문자열로, '/' (U+002F SOLIDUS)로 시작하지 않습니다.
absolute path는 '/' (U+002F SOLIDUS)로 시작하고, 그 뒤에 0개 이상의 path segment가 '/' (U+002F SOLIDUS)로 연결된 문자열입니다.
path는 relative path 또는 absolute path입니다.
valid path는 USVString
타입이며, path입니다.
2.2. 파일 및 디렉터리
file은 바이너리 데이터와 name (비어 있지 않은 name)으로 구성됩니다.
directory는 name (name)과 멤버의 순서 있는 리스트로 구성됩니다. 각 멤버는 file 또는 directory입니다. directory의 각 멤버는 고유한 비어 있지 않은 name을 가져야 합니다.
root directory는 directory이며, 다른 directory의 멤버가 아닙니다. root directory의 name은 비어 있습니다.
parent는 file 또는 directory가 속한 directory입니다. root directory는 parent가 없습니다.
note: 대부분의 경우, 사용자가 선택한 파일과 디렉터리는 실제 네이티브 파일 시스템에 존재하지 않는 가상 루트에 포함된 것처럼 API에서 표시됩니다.
파일 시스템은 이름과 루트(연관된 루트 디렉터리)로 구성됩니다. 이름은 파일 시스템마다 고유한 USVString
타입이며, 구현에 따라 정의됩니다. 루트 디렉터리는
정확히 하나의 파일 시스템에 연관됩니다.
참고: 구현체는 각 이름을 생성할 때, 파일 시스템 인스턴스마다 UUID를 만들고, 고정된 접두사와 접미사를 적용할 수 있습니다. API를 사용하는 작성자는 이름의 구조나 내용에 대해 가정하지 않는 것이 좋습니다.
2.3. 항목
entry는 file entry 또는 directory entry입니다.
entry는 name (name)과 full path (absolute path)를 가집니다.
entry는 root도 가지며, 연관된 root directory입니다.
note: entry는 root directory를 기준으로 한 path로 정의됩니다. 이는 API와 상호작용하는 네이티브 파일 시스템이 디렉터리 내용 열거와 같은 작업 중에 비동기적으로 변경될 수 있음을 고려한 것입니다. entry에 노출된 동작은 path가 더 이상 동일한 엔티티를 참조하지 않을 경우 오류를 발생시킬 수 있습니다.
file system은 entry의 root에 연관된 file system입니다.
3. 알고리즘
abspath (절대 경로)와 path (절대 경로, 상대 경로 또는 빈 문자열)로 상대 경로를 해석하기를 수행하려면 다음 단계를 따른다. 반환값은 절대 경로이다.
-
path가 절대 경로라면, path를 반환한다.
-
abspath segments를 엄격하게 분할하여 abspath를 '/' (U+002F SOLIDUS) 기준으로 나눈 결과로 한다.
note: 첫 번째 문자열은 빈 값이다.
-
path segments를 엄격하게 분할하여 path를 '/' (U+002F SOLIDUS) 기준으로 나눈 결과로 한다.
-
path segments의 각 segment에 대해 다음을 수행한다:
- 빈 문자열
-
계속한다.
- '.' (U+002E FULL STOP)
-
계속한다.
- '..' (U+002E FULL STOP, U+002E FULL STOP)
-
abspath segments의 마지막 멤버를 제거한다. 단, 그 멤버가 유일한 멤버라면 제거하지 않는다.
- 그 외
-
segment를 abspath segments에 추가한다.
-
abspath segments를 '/' (U+002F SOLIDUS)로 연결하여 반환한다.
directory (루트 디렉터리)와 path (절대 경로)로 경로 평가하기를 수행하려면 다음 단계를 따른다. 반환값은 파일, 디렉터리 또는 실패이다.
-
segments를 엄격하게 분할하여 path를 '/' (U+002F SOLIDUS) 기준으로 나눈 결과로 한다.
-
segments의 첫 번째 항목을 제거한다.
note: path가 절대 경로이므로, 첫 번째 항목은 항상 빈 값이다.
-
segments의 각 segment에 대해 다음을 수행한다:
- 빈 문자열
-
계속한다.
- '.' (U+002E FULL STOP)
-
계속한다.
- '..' (U+002E FULL STOP, U+002E FULL STOP)
-
directory를 directory의 상위로 지정한다. 만약 상위가 없다면 directory 자신을 그대로 사용한다.
- 그 외
-
다음 단계를 수행한다:
4. File
인터페이스
partial interface File {readonly attribute USVString webkitRelativePath ; };
webkitRelativePath
getter 단계는 this의 상대 경로를 반환하거나, 지정되지 않았다면 빈
문자열을 반환한다.
5. HTML: 폼
partial interface HTMLInputElement {attribute boolean ;
webkitdirectory readonly attribute FrozenArray <FileSystemEntry >; };
webkitEntries
input
요소의
type
속성이 파일 업로드 상태에 있을 때, 이 섹션의 규칙을 적용한다.
webkitdirectory
속성은 사용자가 파일 또는 파일들 대신 디렉터리를 선택할 수 있도록 허용하는지 여부를 나타내는 불리언 속성이다. 지정된 경우, 디렉터리 선택 시 해당 디렉터리를 상위로 하는 모든 파일이 선택된
것과 동일하게 동작한다. 또한, 각 webkitRelativePath
속성은 File
객체의 상대 경로로, 선택된 디렉터리부터 파일까지의
경로를 포함한다.
documents/ to_upload/ a/ b/ 1.txt 2.txt 3.txt not_uploaded.txt
to_upload
디렉터리를 선택한 경우, files
컬렉션에는 다음이 포함된다:
-
name
== "1.txt
",webkitRelativePath
== "to_upload/a/b/1.txt
"인 항목 -
name
== "2.txt
",webkitRelativePath
== "to_upload/a/b/2.txt
"인 항목 -
name
== "3.txt
",webkitRelativePath
== "to_upload/a/3.txt
"인 항목
note: 사용자 에이전트는 선택 작업 중 계층적 데이터를 디렉터리로 표현할 수 있다. 예를 들어,
네이티브 파일 시스템을 직접 노출하지 않는 장치에서는 사진 앨범을 "image/*"
가 accept
속성에 지정된 경우 디렉터리로 표시할 수 있다.
webkitRelativePath
속성을 디렉터리 선택 후
input
요소에서 검사하는 예:
< input id = b type = file webkitdirectory >
document. querySelector( '#b' ). addEventListener( 'change' , e=> { for ( file entryof e. target. files) { console. log( file. name, file. webkitRelativePath); } });
webkitEntries
IDL 속성은 스크립트가 선택된 항목에 접근할 수 있도록 한다. 이 속성에 접근할 때, 적용된다면 현재 선택된 파일(디렉터리 포함)들을 나타내는 FileSystemEntry
객체 배열을 반환해야 한다. 적용되지 않으면 null을 반환해야 한다.
webkitEntries
를
사용하여 항목을 열거하는 예:
< input id = a type = file multiple >
document. querySelector( '#a' ). addEventListener( 'change' , e=> { for ( const entryof e. target. webkitEntries) { handleEntry( entry); } });
webkitEntries
속성이 드래그 앤 드롭 작업의 결과로만 채워진다. 요소를 클릭할 때는 채워지지 않는다. 항상 채워지도록 수정해야 할까? webkitdirectory
속성이 HTMLInputElement
에
지정된 경우,
webkitEntries
속성이 채워지지 않는다. 대신 files
컬렉션과 webkitRelativePath
속성을 사용해 디렉터리 구조를 재구성해야 한다. 항상 채워지도록 수정해야 할까?
6. HTML: 드래그 앤 드롭
드래그 앤 드롭 작업 중에는 파일과 디렉터리 항목이 항목에 연결됩니다. 각 항목은 루트 디렉터리의 멤버이며, 해당 드래그 데이터 저장소에 고유합니다.
또한, 각 디렉터리 항목은 드래그 데이터 저장소 항목 리스트에서 kind가 File인 항목으로 표현됩니다. getAsFile()
로 접근할 경우 0 바이트 길이의 File
이 반환됩니다.
note: 사용자 에이전트는 드래그 앤 드롭 작업 중에 계층적 데이터를 파일과 디렉터리로 표현할 수 있습니다. 예를 들어, 앨범 메타데이터와 트랙 블롭이 별도의 테이블에 저장된 관계형 데이터베이스의 오디오 데이터를 미디어 플레이어에서 드래그하면 스크립트에서는 디렉터리와 파일로 노출될 수 있습니다.
partial interface DataTransferItem {FileSystemEntry ?webkitGetAsEntry (); };
webkitGetAsEntry()
메서드 단계:
-
store를 this의
DataTransfer
객체의 드래그 데이터 저장소로 한다. -
store의 드래그 데이터 저장소 모드가 읽기/쓰기 모드 또는 읽기 전용 모드가 아니면, null을 반환하고 중단한다.
-
item을 store의 드래그 데이터 저장소 항목 리스트에서 this가 나타내는 항목으로 한다.
-
item의 kind가 File이 아니면 null을 반환하고 중단한다.
-
항목을 나타내는 새로운
FileSystemEntry
객체를 반환한다.
elem. addEventListener( 'dragover' , e=> { // 네비게이션 방지. e. preventDefault(); }); elem. addEventListener( 'drop' , e=> { // 네비게이션 방지. e. preventDefault(); // 모든 항목 처리. for ( const itemof e. dataTransfer. items) { // 파일/디렉터리 항목의 kind는 'file'. if ( item. kind=== 'file' ) { const entry= item. webkitGetAsEntry(); handleEntry( entry); } } });
7. 파일 및 디렉터리
callback =
ErrorCallback undefined (DOMException );
err
ErrorCallback
함수는 비동기적으로 오류를 반환할 수 있는 동작에 사용됩니다.
-
file reading task source [FileAPI]는
readEntries()
성공 및 오류 콜백에 사용됩니다. (Chromium 구현에서는TaskType::kFileReading
이라 부릅니다.) -
기타 위치에서는 DOM manipulation task source [HTML]를 사용합니다. (Chromium 구현에서는
TaskType::kMiscPlatformAPI
를 사용하며 이는TaskType::kDOMManipulation
과 동일한 태스크 큐를 타겟팅합니다.)
7.1. FileSystemEntry
인터페이스
[Exposed =Window ]interface {
FileSystemEntry readonly attribute boolean isFile ;readonly attribute boolean isDirectory ;readonly attribute USVString name ;readonly attribute USVString fullPath ;readonly attribute FileSystem filesystem ;undefined getParent (optional FileSystemEntryCallback ,
successCallback optional ErrorCallback ); };
errorCallback
FileSystemEntry
는 연관된 항목을 가집니다.
isFile
getter 단계는 this가 파일 항목이면 true, 아니면 false를 반환합니다.
isDirectory
getter 단계는 this가 디렉터리 항목이면 true, 아니면 false를 반환합니다.
name
getter 단계는 this의 name을 반환합니다.
fullPath
getter 단계는 this의 full path를 반환합니다.
filesystem
getter 단계는 this의 file system을 반환합니다.
getParent(successCallback, errorCallback)
메서드 단계:
-
병렬로 다음 단계를 실행한다:
-
item이 실패라면 태스크 큐에 추가하여 콜백 함수 호출로 errorCallback(있다면)과 « 새로 생성된 "
NotFoundError
"DOMException
»와 "report
"를 호출하고, 이 단계를 중단한다. -
entry를 item의 name을 name으로, path를 full path로 하는 새로운 디렉터리 항목으로 한다.
-
태스크 큐에 추가하여 콜백 함수 호출로 successCallback과 «
FileSystemDirectoryEntry
객체(연관된 entry) »와 "report
"를 호출한다.
note: FileSystemEntry
생성 이후 디스크의 파일이 변경되었을 때 오류가 발생할 수 있습니다.
function handleEntry( entry) { console. log( 'name: ' + entry. name); console. log( 'path: ' + entry. fullPath); if ( entry. isFile) { console. log( '... 파일입니다' ); } else if ( entry. isDirectory) { console. log( '... 디렉터리입니다' ); } }
getParent()
를 Promise [ECMA-262]와 함께 사용하는 헬퍼 함수 예시:
function getParentAsPromise( entry) { return new Promise(( resolve, reject) => { entry. getParent( resolve, reject); }); }
7.2. FileSystemDirectoryEntry
인터페이스
[Exposed =Window ]interface :
FileSystemDirectoryEntry FileSystemEntry {FileSystemDirectoryReader createReader ();undefined getFile (optional USVString ?,
path optional FileSystemFlags = {},
options optional FileSystemEntryCallback ,
successCallback optional ErrorCallback );
errorCallback undefined getDirectory (optional USVString ?,
path optional FileSystemFlags = {},
options optional FileSystemEntryCallback ,
successCallback optional ErrorCallback ); };
errorCallback dictionary {
FileSystemFlags boolean =
create false ;boolean =
exclusive false ; };callback =
FileSystemEntryCallback undefined (FileSystemEntry );
entry
note: FileSystemFlags
의 create
멤버와 연관된 동작은 기존 구현과의 호환성을 위해 포함되어 있지만, 해당 플래그가 지정되어도 유용한 동작은 없습니다. 마찬가지로 exclusive
멤버는 명시적으로 참조되지 않지만, getter가 있는 객체가 전달된 경우 스크립트에서 바인딩 동작을 관찰할 수 있습니다.
FileSystemDirectoryEntry
의
연관 항목은 디렉터리 항목입니다.
createReader()
메서드 단계:
-
연관된 디렉터리 항목과 연결된 새
FileSystemDirectoryReader
객체를 반환한다.
getFile(path, options, successCallback, errorCallback)
메서드 단계:
-
병렬로 다음 단계를 실행한다:
-
path가 undefined 또는 null이면 path를 빈 문자열로 한다.
-
path가 유효한 경로가 아니면, 태스크 큐에 추가하여 콜백 함수 호출로 errorCallback(있다면)과 « 새로 생성된 "
TypeMismatchError
"DOMException
»와 "report
"를 호출하고, 이 단계를 중단한다. -
options의
create
멤버가 true이면, 태스크 큐에 추가하여 콜백 함수 호출로 errorCallback(있다면)과 « 새로 생성된 "SecurityError
"DOMException
» 와 "report
"를 호출하고, 이 단계를 중단한다. -
item이 실패라면 태스크 큐에 추가하여 콜백 함수 호출로 errorCallback(있다면)과 « 새로 생성된 "
NotFoundError
"DOMException
»와 "report
"를 호출하고, 이 단계를 중단한다. -
item이 파일이 아니면, 태스크 큐에 추가하여 콜백 함수 호출로 errorCallback(있다면)과 « 새로 생성된 "
TypeMismatchError
"DOMException
»와 "report
"를 호출하고, 이 단계를 중단한다. -
entry를 item의 name을 name으로, path를 full path로 하는 새로운 파일 항목으로 한다.
-
태스크 큐에 추가하여 콜백 함수 호출로 successCallback(있다면)과 «
FileSystemFileEntry
객체(연관된 entry) »와 "report
"를 호출한다.
-
getDirectory(path, options, successCallback, errorCallback)
메서드 단계:
-
병렬로 다음 단계를 실행한다:
-
path가 undefined 또는 null이면 path를 빈 문자열로 한다.
-
path가 유효한 경로가 아니면, 태스크 큐에 추가하여 콜백 함수 호출로 errorCallback(있다면)과 « 새로 생성된 "
TypeMismatchError
"DOMException
»와 "report
"를 호출하고, 이 단계를 중단한다. -
options의
create
멤버가 true이면, 태스크 큐에 추가하여 콜백 함수 호출로 errorCallback(있다면)과 « 새로 생성된 "SecurityError
"DOMException
» 와 "report
"를 호출하고, 이 단계를 중단한다. -
item이 실패라면 태스크 큐에 추가하여 콜백 함수 호출로 errorCallback(있다면)과 « 새로 생성된 "
NotFoundError
"DOMException
»와 "report
"를 호출하고, 이 단계를 중단한다. -
item이 디렉터리가 아니면, 콜백 함수 호출로 errorCallback(있다면)과 « 새로 생성된 "
TypeMismatchError
"DOMException
»와 "report
"를 호출하고, 이 단계를 중단한다. -
entry를 item의 name을 name으로, path를 full path로 하는 새로운 디렉터리 항목으로 한다.
-
태스크 큐에 추가하여 콜백 함수 호출로 successCallback(있다면)과 «
FileSystemDirectoryEntry
객체(연관된 entry) »와 "report
"를 호출한다.
-
getFile()
와 getDirectory()
를 Promise [ECMA-262]와 함께 사용하는 헬퍼 함수 예시:
function getFileAsPromise( entry, path) { return new Promise(( resolve, reject) => { entry. getFile( path, {}, resolve, reject); }); } function getDirectoryAsPromise( entry, path) { return new Promise(( resolve, reject) => { entry. getDirectory( path, {}, resolve, reject); }); }
7.3. FileSystemDirectoryReader
인터페이스
[Exposed =Window ]interface {
FileSystemDirectoryReader undefined (
readEntries FileSystemEntriesCallback ,
successCallback optional ErrorCallback ); };
errorCallback callback =
FileSystemEntriesCallback undefined (sequence <FileSystemEntry >);
entries
FileSystemDirectoryReader
는
연관된 entry (디렉터리 항목),
연관된 directory (초기값 null),
reading flag (초기값 false),
done flag (초기값 false),
reader error (초기값 null)을 가진다.
readEntries(successCallback, errorCallback)
메서드 단계:
-
this의 reading flag가 true이면, 태스크 큐에 추가하여 콜백 함수 호출로 errorCallback과 « 새로 생성된 "
InvalidStateError
"DOMException
»와 "report
"를 호출하고, 이 단계를 중단한다. -
this의 reader error가 null이 아니면, 태스크 큐에 추가하여 콜백 함수 호출로 errorCallback(있다면)과 « reader error »와 "
report
"를 호출하고, 이 단계를 중단한다. -
this의 done flag가 true이면, 태스크 큐에 추가하여 콜백 함수 호출로 successCallback과 빈 리스트와 "
report
"를 호출하고, 이 단계를 중단한다. -
this의 reading flag를 true로 설정한다.
-
병렬로 다음 단계를 실행한다:
-
-
dir이 실패라면:
-
태스크 큐에 추가하여 다음을 수행한다:
-
error를 새로 생성된 "
NotFoundError
"DOMException
로 한다. -
this의 reader error를 error로 설정한다.
-
this의 reading flag를 false로 설정한다.
-
콜백 함수 호출 errorCallback(있다면)과 « error » 와 "
report
"를 호출한다.
-
-
이 단계를 중단한다.
-
-
entries를 this의 directory에서 아직 이
FileSystemDirectoryReader
에 의해 반환되지 않은 멤버 중 일부(0개 이상)로 한다. -
이전 단계가 실패했다면(예: 디렉터리가 삭제되었거나 권한이 거부된 경우):
-
태스크 큐에 추가하여 다음을 수행한다:
-
error를 적절한
DOMException
으로 한다. -
this의 reader error를 error로 설정한다.
-
this의 reading flag를 false로 설정한다.
-
콜백 함수 호출 errorCallback(있다면)과 « error »와 "
report
"를 호출한다.
-
-
이 단계를 중단한다.
-
-
태스크 큐에 추가하여 다음을 수행한다:
-
this의 reading flag를 false로 설정한다.
-
콜백 함수 호출 successCallback과 « entries »와 "
report
"를 호출한다.
-
참고: reading flag의 사용으로 위 병렬 단계가 동시에 실행되는 것을 방지합니다. 별도의 병렬 큐 지정이 필요하지 않습니다.
const reader= dirEntry. createReader(); const doBatch= () => { // 배치 읽기. reader. readEntries( entries=> { // 완료? if ( entries. length=== 0 ) { return ; } // 배치 처리. entries. forEach( handleEntry); // 다음 배치 읽기. doBatch(); }, error=> console. warn( error)); }; // 읽기 시작 doBatch();
FileSystemDirectoryReader
를 Promise [ECMA-262]와 함께 사용하는 헬퍼 함수 예시:
function getEntriesAsPromise( dirEntry) { return new Promise(( resolve, reject) => { const result= []; const reader= dirEntry. createReader(); const doBatch= () => { reader. readEntries( entries=> { if ( entries. length> 0 ) { entries. forEach( e=> result. push( e)); doBatch(); } else { resolve( result); } }, reject); }; doBatch(); }); }
FileSystemDirectoryReader
를 AsyncIterators [ECMA-262]와 함께 사용하는 헬퍼 함수 예시:
async function * getEntriesAsAsyncIterator( dirEntry) { const reader= dirEntry. createReader(); const getNextBatch= () => new Promise(( resolve, reject) => { reader. readEntries( resolve, reject); }); let entries; do { entries= await getNextBatch(); for ( const entryof entries) { yield entry; } } while ( entries. length> 0 ); }
이렇게 하면 for-await-of
를 사용해 디렉터리 트리를 순서대로 비동기적으로 탐색할 수 있습니다:
async function show( entry) { console. log( entry. fullPath); if ( entry. isDirectory) { for await ( const eof getEntriesAsAsyncIterator( entry)) { await show( e); } } }
7.4. FileSystemFileEntry
인터페이스
[Exposed =Window ]interface :
FileSystemFileEntry FileSystemEntry {undefined file (FileCallback ,
successCallback optional ErrorCallback ); };
errorCallback callback =
FileCallback undefined (File );
file
FileSystemFileEntry
의
연관 항목은 파일 항목입니다.
file(successCallback, errorCallback)
메서드 단계:
-
병렬로 다음 단계를 실행한다:
-
item이 실패라면, 태스크 큐에 추가하여 콜백 함수 호출로 errorCallback(있다면)과 « 새로 생성된 "
NotFoundError
"DOMException
»와 "report
"를 호출하고, 이 단계를 중단한다. -
item이 디렉터리이면, 태스크 큐에 추가하여 콜백 함수 호출로 errorCallback(있다면)과 « 새로 생성된 "
TypeMismatchError
"DOMException
»와 "report
"를 호출하고, 이 단계를 중단한다. -
태스크 큐에 추가하여 콜백 함수 호출로 successCallback과 «
File
객체(item에 해당) »와 "report
"를 호출한다.
FileReader
를
사용해 드롭된 파일의 내용을 읽는 예시:
function readFileEntry( entry) { entry. file( file=> { const reader= new FileReader(); reader. readAsText( file); reader. onerror= error=> console. warn( error); reader. onload= () => { console. log( reader. result); }; }, error=> console. warn( error)); }
file()
를 Promise [ECMA-262]와 함께 사용하는 헬퍼 함수 예시:
function fileAsPromise( entry) { return new Promise(( resolve, reject) => { entry. file( resolve, reject); }); }
7.5. FileSystem
인터페이스
[Exposed =Window ]interface {
FileSystem readonly attribute USVString name ;readonly attribute FileSystemDirectoryEntry root ; };
FileSystem
는 연관된 파일 시스템을 가집니다.
name
getter 단계는 this의 name을 반환합니다.
root
getter 단계는 FileSystemDirectoryEntry
객체(연관된 this의
root)를 반환합니다.
8. 감사의 글
이 명세서는 [file-system-api]에서 Eric Uhrhane이 작성한 작업에 크게 기반하고 있으며, 그
작업에서 FileSystemEntry
타입을 도입하였습니다.
이 문서 작성에 사용된 명세 저작 도구 Bikeshed를 만들고 유지 관리한 Tab Atkins, Jr.에게 감사드립니다.
그리고 Ali Alabbas, Philip Jägenstedt, Marijn Kruisselbrink, Olli Pettay, 그리고 Kent Tamura 등에게 제안, 리뷰, 기타 피드백을 주셔서 감사합니다.