웹은 기본적으로 단일 스레드이지만 WebWork를 통해 멀티 스레드를 사용할 수 있다. 별도의 스레드를 통해 어떤 그림을 그릴 수 있다면 그림이 그려지는 동안에도 다른 작업을 처리할 수 있다.
시나리오는 다음과 같다. 사용자는 자신의 PC에서 이미지 파일을 읽고 이미지 파일에 대한 데이터를 웹 워커에게 전달한다. 웹워커에서 이 이미지 데이터를 캔버스에 그린다. (사실 이 예제는 흐름상 묘한데, 이미지 데이터를 캔버스에 그린다라는 것을 캔버스에 원하는 도형들을 그린다라고 하는게 더 자연스럽다.)
먼저 UI로 이미지 파일을 읽어올 DOM이 필요하다. 이 코드는 main.js 파일에 존재한다.
document.querySelector('#app').innerHTML = /* html */ `
<input type="file" />
`;
웹워크를 기동한다. 이 코드는 main.js 파일에 존재한다.
const worker = new Worker('./worker.js');
input DOM을 클릭해서 파일을 선택했을때의 이벤트는 다음과 같다.
const handleFile = (event) => {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = (event) => {
const canvas = document.createElement("canvas");
const offsetScreenCanvas = canvas.transferControlToOffscreen();
const dataUrl = event.target.result;
worker.postMessage(
{ offsetScreenCanvas, dataUrl },
[ offsetScreenCanvas ]
);
}
reader.readAsDataURL(file);
}
const fileInput = document.querySelector("input");
fileInput.addEventListener('change', handleFile);
canavs를 만들고 이 캔버스를 웹워커로 전달하기 위해 offsetScreen으로 만든다. offsetScreen은 공유 메모리로 전달할 수 있지만 읽은 파일의 데이터는 복사해서 전달하고 있다. work.js 파일의 내용은 다음과 같다.
self.onmessage = async (event) => {
const { offsetScreenCanvas, dataUrl } = event.data;
// const response = await fetch(dataUrl);
// const blob = await response.blob();
const blob = await fetch(dataUrl).then((r) => r.blob());
const imageBitmap = await createImageBitmap(blob);
const context = offsetScreenCanvas.getContext('2d');
const width = imageBitmap.width / 2;
const height = imageBitmap.height / 2;
offsetScreenCanvas.width = width;
offsetScreenCanvas.height = height;
context.drawImage(imageBitmap, 0, 0, width, height);
}
이미지를 캔버스에 다 그렇다면 캔버스에 그려진 결과를 Blob 데이터르 만들어 메인 스레드에 전달해야 한다. 관련 코드는 다음과 같다.
self.onmessage = async (event) => {
...
context.drawImage(imageBitmap, 0, 0, width, height);
offsetScreenCanvas.convertToBlob().then(blob => {
const reader = new FileReader();
reader.onload = () => {
self.postMessage({ dataUrl: reader.result });
};
reader.readAsDataURL(blob);
});
}
위의 코드와 관련된 메인 스레드의 코드는 다음과 같다.
worker.onmessage = (e) => {
const img = new Image();
img.src = e.data.dataUrl;
document.querySelector('#app').appendChild(img);
}
앞서 언급했듯 위의 예제에서 가장 큰 문제는 메인 스레드에서 읽은 이미지 데이터 원본에 대한 동일한 크기의 데이터를 만들어 웹워커에 전달하고 있다. 이 부분에 대한 개선은 공유 메모리로 해결이 가능하다. 단, 공유 메모리를 사용하기 위해서는 서버 측 보안 수준이 가장 높은 상태여야 한다. 끝으로 최종 결과는 아래와 같다.

