worker용 동기 파일 시스템 API

소개

HTML5 FileSystem API웹 작업자는 그 자체로 매우 강력합니다. FileSystem API는 마침내 웹 애플리케이션에 계층적 저장소 및 파일 I/O를 제공하고 Worker는 JavaScript에 진정한 비동기 '멀티스레딩'을 제공합니다. 하지만 이러한 API를 함께 사용하면 정말 흥미로운 앱을 빌드할 수 있습니다.

이 튜토리얼에서는 웹 작업자 내에서 HTML5 파일 시스템을 활용하기 위한 가이드와 코드 예제를 제공합니다. 두 API에 대한 실무 지식이 있다고 가정합니다. 아직 시작할 준비가 되지 않았거나 이러한 API에 관해 자세히 알아보고 싶다면 기본사항을 다루는 두 가지 유용한 튜토리얼인 FileSystem API 살펴보기웹 워커의 기본사항을 읽어보세요.

동기 API와 비동기 API 비교

비동기 JavaScript API는 사용하기 어려울 수 있습니다. 크기가 큽니다. 그들은 복잡합니다. 하지만 가장 불만스러운 점은 문제가 발생할 가능성이 많다는 것입니다. 마지막으로 처리해야 할 것은 이미 비동기식 환경 (작업자)에서 복잡한 비동기 API (FileSystem)를 레이어링하는 것입니다. 다행히 FileSystem API는 동기 버전을 정의하여 웹 작업자의 어려움을 덜어줍니다.

대부분의 경우 동기식 API는 비동기식 API와 정확히 동일합니다. 메서드, 속성, 특징 및 기능은 익숙할 것입니다. 주요 편차는 다음과 같습니다.

  • 동기 API는 Web Worker 컨텍스트 내에서만 사용할 수 있는 반면 비동기 API는 Worker 내부 및 외부에서 사용할 수 있습니다.
  • 콜백이 나갑니다. 이제 API 메서드가 값을 반환합니다.
  • 창 객체의 전역 메서드(requestFileSystem()resolveLocalFileSystemURL())가 requestFileSystemSync()resolveLocalFileSystemSyncURL()이 됩니다.

이러한 예외 외에는 API가 동일합니다. 좋습니다. 이제 시작할 수 있습니다.

파일 시스템 요청

웹 애플리케이션은 웹 작업자 내에서 LocalFileSystemSync 객체를 요청하여 동기 파일 시스템에 대한 액세스 권한을 얻습니다. requestFileSystemSync()는 Worker의 전역 범위에 노출됩니다.

var fs = requestFileSystemSync(TEMPORARY, 1024*1024 /*1MB*/);

이제 동기 API를 사용하고 있으므로 새 반환 값과 성공 및 오류 콜백이 없음을 확인할 수 있습니다.

일반 FileSystem API와 마찬가지로 현재 메서드 앞에 접두사가 추가됩니다.

self.requestFileSystemSync = self.webkitRequestFileSystemSync ||
                                 self.requestFileSystemSync;

할당량 처리

현재 Worker 컨텍스트에서는 PERSISTENT 할당량을 요청할 수 없습니다. Workers 외부에서 할당량 문제를 해결하는 것이 좋습니다. 프로세스는 다음과 같이 표시될 수 있습니다.

  1. worker.js: FileSystem API 코드를 try/catch로 래핑하여 QUOTA_EXCEED_ERR 오류가 포착되도록 합니다.
  2. worker.js: QUOTA_EXCEED_ERR을 포착하면 postMessage('get me more quota')를 기본 앱으로 다시 전송합니다.
  3. 기본 앱: 2번을 수신하면 window.webkitStorageInfo.requestQuota() 댄스를 진행합니다.
  4. 기본 앱: 사용자가 추가 할당량을 부여한 후 postMessage('resume writes')를 다시 작업자로 보내 추가 저장공간을 알립니다.

꽤 복잡한 해결 방법이지만 작동할 것입니다. FileSystem API로 PERSISTENT 스토리지를 사용하는 방법에 관한 자세한 내용은 할당량 요청을 참고하세요.

파일 및 디렉터리 작업

getFile()getDirectory()의 동기 버전은 각각 FileEntrySyncDirectoryEntrySync를 반환합니다.

예를 들어 다음 코드는 루트 디렉터리에 'log.txt'라는 빈 파일을 만듭니다.

var fileEntry = fs.root.getFile('log.txt', {create: true});

다음은 루트 폴더에 새 디렉터리를 만듭니다.

var dirEntry = fs.root.getDirectory('mydir', {create: true});

오류 처리

웹 워커 코드를 디버그한 적이 없다면 부럽습니다. 문제가 무엇인지 파악하는 것은 정말 힘든 일일 수 있습니다.

동기식 환경에는 오류 콜백이 없으므로 문제를 처리하는 것이 생각보다 까다로워집니다. Web Worker 코드 디버깅이 일반적인 복잡성을 더하면 금방 좌절감을 느낄 것입니다. 관련 Worker 코드를 모두 try/catch로 래핑하면 작업이 더 쉬워집니다. 그런 다음 오류가 발생하면 postMessage()를 사용하여 오류를 기본 앱으로 전달합니다.

function onError(e) {
    postMessage('ERROR: ' + e.toString());
}

try {
    // Error thrown if "log.txt" already exists.
    var fileEntry = fs.root.getFile('log.txt', {create: true, exclusive: true});
} catch (e) {
    onError(e);
}

파일, Blob, ArrayBuffer 전달

웹 워커가 처음 등장했을 때는 postMessage()에서 문자열 데이터만 전송할 수 있었습니다. 나중에 브라우저가 직렬화 가능한 데이터를 허용하기 시작했습니다. 즉, JSON 객체를 전달할 수 있었습니다. 하지만 최근에는 Chrome과 같은 일부 브라우저에서 구조화된 클론 알고리즘을 사용하여 postMessage()를 통해 전달되는 더 복잡한 데이터 유형을 허용합니다.

이것이 의미하는 바는 무엇일까요? 즉, 기본 앱과 작업자 스레드 간에 바이너리 데이터를 전달하는 것이 훨씬 쉽습니다. 워커의 구조화된 클론을 지원하는 브라우저를 사용하면 유형이 지정된 배열, ArrayBuffer, File 또는 Blob를 워커에 전달할 수 있습니다. 데이터는 여전히 사본이지만 File를 전달할 수 있으면 postMessage()에 전달하기 전에 파일을 base64로 인코딩하는 이전 접근 방식에 비해 성능 이점이 있습니다.

다음 예에서는 사용자가 선택한 파일 목록을 전용 Worker에 전달합니다. 작업자는 파일 목록을 전달하기만 하면 되며(반환된 데이터가 실제로 FileList임을 간단하게 표시할 수 있음) 기본 앱은 각 파일을 ArrayBuffer로 읽습니다.

또한 이 샘플은 웹 워커의 기본사항에 설명된 인라인 웹 워커 기법의 개선된 버전을 사용합니다.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="chrome=1">
    <title>Passing a FileList to a Worker</title>
    <script type="javascript/worker" id="fileListWorker">
    self.onmessage = function(e) {
    // TODO: do something interesting with the files.
    postMessage(e.data); // Pass through.
    };
    </script>
</head>
<body>
</body>

<input type="file" multiple>

<script>
document.querySelector('input[type="file"]').addEventListener('change', function(e) {
    var files = this.files;
    loadInlineWorker('#fileListWorker', function(worker) {

    // Setup handler to process messages from the worker.
    worker.onmessage = function(e) {

        // Read each file aysnc. as an array buffer.
        for (var i = 0, file; file = files[i]; ++i) {
        var reader = new FileReader();
        reader.onload = function(e) {
            console.log(this.result); // this.result is the read file as an ArrayBuffer.
        };
        reader.onerror = function(e) {
            console.log(e);
        };
        reader.readAsArrayBuffer(file);
        }

    };

    worker.postMessage(files);
    });
}, false);


function loadInlineWorker(selector, callback) {
    window.URL = window.URL || window.webkitURL || null;

    var script = document.querySelector(selector);
    if (script.type === 'javascript/worker') {
    var blob = new Blob([script.textContent]);
    callback(new Worker(window.URL.createObjectURL(blob));
    }
}
</script>
</html>

Worker에서 파일 읽기

작업자에서 비동기 FileReader API를 사용하여 파일을 읽는 것이 좋습니다. 하지만 더 나은 방법이 있습니다. Worker에는 파일 읽기를 간소화하는 동기식 API(FileReaderSync)가 있습니다.

기본 앱:

<!DOCTYPE html>
<html>
<head>
    <title>Using FileReaderSync Example</title>
    <style>
    #error { color: red; }
    </style>
</head>
<body>
<input type="file" multiple />
<output id="error"></output>
<script>
    var worker = new Worker('worker.js');

    worker.onmessage = function(e) {
    console.log(e.data); // e.data should be an array of ArrayBuffers.
    };

    worker.onerror = function(e) {
    document.querySelector('#error').textContent = [
        'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message].join('');
    };

    document.querySelector('input[type="file"]').addEventListener('change', function(e) {
    worker.postMessage(this.files);
    }, false);
</script>
</body>
</html>

worker.js

self.addEventListener('message', function(e) {
    var files = e.data;
    var buffers = [];

    // Read each file synchronously as an ArrayBuffer and
    // stash it in a global array to return to the main app.
    [].forEach.call(files, function(file) {
    var reader = new FileReaderSync();
    buffers.push(reader.readAsArrayBuffer(file));
    });

    postMessage(buffers);
}, false);

예상대로 동기식 FileReader를 사용하면 콜백이 사라집니다. 이렇게 하면 파일을 읽을 때 콜백 중첩의 양이 단순화됩니다. 대신 readAs* 메서드는 읽은 파일을 반환합니다.

예: 모든 항목 가져오기

경우에 따라 동기식 API가 특정 작업에 훨씬 더 적합할 수 있습니다. 콜백이 적을수록 가독성이 향상됩니다. 동기식 API의 진짜 단점은 Worker의 제한사항에서 비롯됩니다.

보안상의 이유로 호출 앱과 웹 워커 스레드 간의 데이터는 공유되지 않습니다. 데이터는 postMessage()가 호출될 때 항상 Worker 간에 복사됩니다. 따라서 모든 데이터 유형을 전달할 수 있는 것은 아닙니다.

안타깝게도 FileEntrySyncDirectoryEntrySync는 현재 허용되는 유형에 속하지 않습니다. 그렇다면 통화 앱으로 항목을 다시 가져오는 방법은 무엇인가요? 제한을 우회하는 한 가지 방법은 항목 목록 대신 filesystem: URL의 목록을 반환하는 것입니다. filesystem: URL은 문자열이므로 전달하기가 매우 쉽습니다. 또한 resolveLocalFileSystemURL()를 사용하여 기본 앱의 항목으로 확인할 수 있습니다. 그러면 FileEntrySync/DirectoryEntrySync 객체로 돌아갑니다.

기본 앱:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<title>Listing filesystem entries using the synchronous API</title>
</head>
<body>
<script>
    window.resolveLocalFileSystemURL = window.resolveLocalFileSystemURL ||
                                        window.webkitResolveLocalFileSystemURL;

    var worker = new Worker('worker.js');
    worker.onmessage = function(e) {
    var urls = e.data.entries;
    urls.forEach(function(url, i) {
        window.resolveLocalFileSystemURL(url, function(fileEntry) {
        console.log(fileEntry.name); // Print out file's name.
        });
    });
    };

    worker.postMessage({'cmd': 'list'});
</script>
</body>
</html>

worker.js

self.requestFileSystemSync = self.webkitRequestFileSystemSync ||
                                self.requestFileSystemSync;

var paths = []; // Global to hold the list of entry filesystem URLs.

function getAllEntries(dirReader) {
    var entries = dirReader.readEntries();

    for (var i = 0, entry; entry = entries[i]; ++i) {
    paths.push(entry.toURL()); // Stash this entry's filesystem: URL.

    // If this is a directory, we have more traversing to do.
    if (entry.isDirectory) {
        getAllEntries(entry.createReader());
    }
    }
}

function onError(e) {
    postMessage('ERROR: ' + e.toString()); // Forward the error to main app.
}

self.onmessage = function(e) {
    var data = e.data;

    // Ignore everything else except our 'list' command.
    if (!data.cmd || data.cmd != 'list') {
    return;
    }

    try {
    var fs = requestFileSystemSync(TEMPORARY, 1024*1024 /*1MB*/);

    getAllEntries(fs.root.createReader());

    self.postMessage({entries: paths});
    } catch (e) {
    onError(e);
    }
};

예: XHR2를 사용하여 파일 다운로드

Worker의 일반적인 사용 사례는 XHR2를 사용하여 여러 파일을 다운로드하고 이러한 파일을 HTML5 파일 시스템에 쓰는 것입니다. 이는 Worker 스레드에 적합한 작업입니다.

다음 예에서는 파일 하나만 가져와서 작성하지만 이를 확장하여 파일 세트를 다운로드할 수 있습니다.

기본 앱:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="chrome=1">
<title>Download files using a XHR2, a Worker, and saving to filesystem</title>
</head>
<body>
<script>
    var worker = new Worker('downloader.js');
    worker.onmessage = function(e) {
    console.log(e.data);
    };
    worker.postMessage({fileName: 'GoogleLogo',
                        url: 'googlelogo.png', type: 'image/png'});
</script>
</body>
</html>

downloader.js:

self.requestFileSystemSync = self.webkitRequestFileSystemSync ||
                                self.requestFileSystemSync;

function makeRequest(url) {
    try {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', url, false); // Note: synchronous
    xhr.responseType = 'arraybuffer';
    xhr.send();
    return xhr.response;
    } catch(e) {
    return "XHR Error " + e.toString();
    }
}

function onError(e) {
    postMessage('ERROR: ' + e.toString());
}

onmessage = function(e) {
    var data = e.data;

    // Make sure we have the right parameters.
    if (!data.fileName || !data.url || !data.type) {
    return;
    }
    
    try {
    var fs = requestFileSystemSync(TEMPORARY, 1024 * 1024 /*1MB*/);

    postMessage('Got file system.');

    var fileEntry = fs.root.getFile(data.fileName, {create: true});

    postMessage('Got file entry.');

    var arrayBuffer = makeRequest(data.url);
    var blob = new Blob([new Uint8Array(arrayBuffer)], {type: data.type});

    try {
        postMessage('Begin writing');
        fileEntry.createWriter().write(blob);
        postMessage('Writing complete');
        postMessage(fileEntry.toURL());
    } catch (e) {
        onError(e);
    }

    } catch (e) {
    onError(e);
    }
};

결론

웹 작업자는 HTML5에서 활용도가 낮고 그다지 인정받지 못하는 기능입니다. 제가 이야기한 대부분의 개발자는 추가적인 계산상의 이점이 필요하지 않지만, 순수한 계산 이상의 용도로 사용할 수 있습니다. 저와 마찬가지로 회의적인 생각이 드신다면 이 도움말이 마음을 바꾸는 데 도움이 되었기를 바랍니다. 디스크 작업 (파일 시스템 API 호출) 또는 작업자에 대한 HTTP 요청 등을 오프로드하는 것은 당연한 것이며 코드를 분류하는 데에도 도움이 됩니다. Workers에 포함된 HTML5 File API는 많은 사람들이 아직 살펴보지 못한 웹 앱의 놀라운 기능을 새롭게 열어줍니다.