Offscreen Canvas 샘플 코드

웹은 기본적으로 단일 스레드이지만 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);
}

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

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다