웹은 기본적으로 단일 스레드이지만 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); }
앞서 언급했듯 위의 예제에서 가장 큰 문제는 메인 스레드에서 읽은 이미지 데이터 원본에 대한 동일한 크기의 데이터를 만들어 웹워커에 전달하고 있다. 이 부분에 대한 개선은 공유 메모리로 해결이 가능하다. 단, 공유 메모리를 사용하기 위해서는 서버 측 보안 수준이 가장 높은 상태여야 한다. 끝으로 최종 결과는 아래와 같다.