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

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

ES6의 Fetch API

ES6에서 지원하는 fetch API는 ES6의 Promise와 함께 AJAX를 Wrapping 해주는 매우 가독성이 뛰어난 방식입니다. fetch API은 내부적으로 AJAX API와 Promise를 사용하면서 외부로 들어내지 않습니다. 이 글은 fetch API에 대한 매우 기초적이지만 가장 일반적으로 많이 사용되는 내용을 정리합니다.

먼저 아래의 코드는 이미지를 fetch API를 통해 가져와 표시하는 코드 중 먼저 DOM 요소에 대한 코드입니다.


언급할 필요조차 없을 정도로 간단합니다. 다음은 실제 관심이 집중되는 fetch API 코드입니다.

fetch('lion.jpg')
    .then(function (response) {
        return response.blob();
    })
    .then(function (blob) {
        var objUrl = URL.createObjectURL(blob);
        myImage.src = objUrl;
    });

먼저 1번 코드의 fetch 함수를 통해 인자로 lion.jpg라는 상대 경로의 URL을 통해 (네트워크에서) 이미지 데이터를 가져와 2번 코드의 첫번째 then에 지정된 함수의 response에 그 결과를 인자로 담아 함수를 호출합니다. 호출된 함수에서 반환하는 결과에 대해 5번 코드의 then에 지정된 또 다른 함수의 인자에 담아 호출하게 됩니다. 하나의 fetch는 이처럼 2개의 연속된 then으로 구성됩니다.

API의 실행에 있어 중요한 것은 해당 API의 실패에 대한 처리입니다. 만약 lion.jpg에 대한 URL이 올바르지 않을 경우에 대한 처리가 필요합니다. 아래의 코드는 이러한 예외에 대한 경우의 코드까지 담고 있습니다.

fetch('lion.jpg')
    .then(function (response) {
        if (response.ok) {
            return response.blob();
        } else {
            alert('네트워크 오류');
        }
    })
    .then(function (blob) {
        var objUrl = URL.createObjectURL(blob);
        myImage.src = objUrl;
    })
    .catch(function (err) {
        alert(err)
    });

첫번째 then은 네트워크 통신 및 인자로 지정한 URL에 대한 문제가 있는지를 검사해야 하는데, 3번 처럼 response 인자의 ok 속성을 통해 확인 가능합니다. 두번째 then은 첫번째 then이 성공했던 실패했든지간에 두번째 then에서 지정한 함수의 실행 시에 어떤 문제가 발생하면 13번 코드의 catch의 함수가 호출됩니다.

아래는 POST 방식에 대한 fetch API 사용예입니다.

var params = {
    '_id': 'test.getPlayersWhere2',
    'age': 10,
    'name': '%'
};

fetch('http://localhost:8080/Bang?query',
    {
        method: 'POST',
        body: JSON.stringify(params)
    })
    .then(function (response) {
        if (response.ok) return response.text();
    })
    .then(function (text) {
        if (text) div.innerText = text;
        else alert('error');
     });

위의 사용예에 대한 jQuery 방식은 아래와 같습니다.

var params = {
    '_id': 'test.getPlayersWhere2',
    'age': 10,
    'name': '%'
};

$.ajax({
    url: 'http://localhost:8080/Bang?query',
    type: 'POST',
    data: JSON.stringify(params),
    dataType: "text",

    success: function (response) {
        div.innerText = response;
    },

    error: function (xhr, status) {
        alert('Error: ' + status);
    }
});