worker용 동기 파일 시스템 API

소개

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

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

동기 API와 비동기 API 비교

비동기 JavaScript API는 사용하기 어려울 수 있습니다. 몸집이 큽니다. 복잡합니다. 하지만 가장 큰 실망은 그들이 잘못될 수 있는 많은 기회를 제공한다는 점입니다. 마지막으로 살펴볼 것은 이미 비동기화된 환경 (Workers)에서 복잡한 비동기 API (FileSystem)에 레이어링하는 것입니다. 좋은 소식은 FileSystem API가 웹 작업자의 고충을 덜어 주기 위해 동기 버전을 정의한다는 것입니다.

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

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

이러한 예외를 제외하면 API는 동일합니다. 자, 이제 가 볼까요!

파일 시스템 요청

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

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

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

일반 FileSystem API와 마찬가지로 이 시점에는 메서드에 접두사가 붙습니다.

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

할당량 처리

현재 작업자 컨텍스트에서는 PERSISTENT 할당량을 요청할 수 없습니다. 작업자 외부에서 할당량 문제를 처리하는 것이 좋습니다. 이 프로세스는 다음과 같습니다.

  1. worker.js: QUOTA_EXCEED_ERR 오류를 포착하도록 FileSystem API 코드를 try/catch에 래핑합니다.
  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 코드를 디버그할 필요가 없었다면 정말 부러워요! 무엇이 잘못되었는지 파악하는 것은 정말 어려운 일입니다.

동기 환경에서는 오류 콜백이 없으므로 문제를 처리하기가 더 어려워집니다. 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, ArrayBuffers 전달

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

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

다음 예시에서는 사용자가 선택한 파일 목록을 전용 작업자에 전달합니다. 작업자는 단순히 파일 목록을 통과하고 (반환된 데이터가 실제로는 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>

작업자에서 파일 읽기

작업자에서 비동기 FileReader API를 사용하여 파일을 읽기는 완전히 허용됩니다. 하지만 더 좋은 방법이 있습니다. 작업자에는 파일 읽기를 간소화하는 동기 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의 진정한 단점은 작업자의 제한사항에서 비롯됩니다.

보안상의 이유로 호출 앱과 웹 작업자 스레드 간의 데이터는 절대 공유되지 않습니다. 데이터는 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를 사용하여 파일 다운로드

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

다음 예에서는 하나의 파일만 가져오고 쓰지만 이미지를 확장하여 파일 세트를 다운로드할 수 있습니다.

기본 앱:

<!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);
    }
};

결론

Web Workers는 HTML5의 제대로 활용도가 떨어지고 제대로 인정받지 못하고 있는 기능입니다. 제가 대화하는 개발자 대부분은 추가적인 컴퓨팅 이점이 필요하지 않지만, 단순한 컴퓨팅 이상의 용도로 사용될 수 있습니다 (저처럼) 의심이 된다면 이 글이 마음을 바꾸는 데 도움이 되었기를 바랍니다. 디스크 작업 (Filesystem API 호출) 또는 HTTP 요청과 같은 항목을 작업자에 오프로드하는 것이 자연스럽고 코드를 분류하는 데에도 도움이 됩니다. Workers 내의 HTML5 File API는 많은 사람이 살펴보지 못한 웹 앱을 위한 완전히 새로운 지평을 넓혀줍니다.