간단하게 요약하자면 OffScreenCanvas는 메인스레드가 아닌 백그라운드 스레드에서 canvas를 다루는 기술입니다.
그렇다면 백그라운드 스레드에서 canvas를 다루는 게 무슨 이점이 있을까요?
canvas는 그래픽을 다루기 때문에 대체로 많은 연산을 필요로 합니다.
이미지에 필터를 적용하거나 애니메이션을 구현하는 등의 기능은 매우 많은 픽셀에 대한 계산
이 이루어지기 때문이죠.
계산이 많으면 무슨 일이 일어날까요? 바로 메인 스레드가 차단이 됩니다.
쉽게 말해 canvas의 그리기를 기다리는 동안 유저는 아무것도 할 수가 없다는 거죠. (무한 루프에 빠져 브라우저가 멈춘 경험이 다들 있을겁니다.)
canvas의 애니메이션이 진행되는 동안 메인스레드에서 무거운 연산이 진행되면, canvas의 애니메이션이 멈출 수 있습니다.
그에 반해 offScreenCanvas는 메인스레드가 아닌 백그라운드 스레드(web worker)에서 렌더링하여 스레드 차단에 대응하기 때문에 유저에게 좋은 경험을 줄 수 있습니다.
offScreenCanvas를 사용하기전에 우선 web worker 에 대한 이해가 필요합니다.
web worker는 백그라운드 스레드에서 작동하기 때문에 전역 객체가 window가 아닙니다.
별도의 DedicatedWorkerGlobalScope라는 객체를 전역으로 가집니다.
이런 특징 때문에 web worker에서의 작업은 기존에 하던 것과는 조금 달라질 수 있습니다.
위에서 말한 offscreenCanvas의 장점은 사실 web worker의 장점이라고 볼 수 있습니다.
바로 무거운 연산을 백그라운드에서 실행하여 메인 스레드의 차단을 최소화
하는 것입니다.
그럼 이제 어떤식으로 web worker와 offscreencanvas를 사용하는지 예제를 통해 알아보겠습니다.
첫번째로 알아볼 예제는 유저가 업로드한 이미지를 offscreenCanvas로 그리는 로직입니다.
index.html
<!DOCTYPE html>
<html>
<head>
<title>Image Drawing</title>
</head>
<body>
<input type="file" id="fileInput" />
<canvas id="canvas" />
<script>
const worker = new Worker('worker.js');
const handleFile = (event) => {
const file = event.target.files[0];
const reader = new FileReader();
reader.onload = (event) => {
const canvas = document.getElementById('canvas');
const offscreenCanvas = canvas.transferControlToOffscreen();
worker.postMessage(
{ canvas: offscreenCanvas, src: event.target.result },
[offscreenCanvas],
);
document.body.appendChild(canvas);
};
reader.readAsDataURL(file);
}
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', handleFile);
</script>
</body>
</html>
우선 worker객체를 만들어야합니다.
root디렉토리에 worker.js를 만들고 생성자를 통해 객체를 생성합니다.
그 다음은 유저의 이미지를 핸들링하는 change 이벤트 함수 handleFile입니다.
이미지가 로드되면 canvas엘리먼트에서 offscreen 객체를 생성한 후에 postMessage함수로 worker에 데이터를 전송합니다
.
worker.postMessage({ canvas: offscreenCanvas, src: event.target.result }, [offscreenCanvas]);
첫번째 인수는 보내고 싶은 데이터고, 두번째 인수는 소유권을 백그라운드 스레드로 이전할 transferble한 객체
를 배열 형태로 전송합니다.
(당연히 worker에서도 메인 스레드로 데이터를 전송할 수 있습니다.)
이렇게 되면 web worker에 canvas와 이미지 src를 전송한 것입니다.
전송을 했으니 이제 web worker에서 데이터를 수신하는 코드를 보겠습니다.
self.onmessage = async function (event) {
const { src, canvas } = event.data;
};
web worker에서는 이런식으로 data를 수신합니다.
메인스레드에서 postMessage를 실행하면 web worker의 onmessage가 트리거
되는 것이죠.
받아온 데이터를 토대로 이미지를 canvas에 렌더링 해보겠습니다.
worker.js
self.onmessage = function (event) {
const { src, canvas } = event.data;
const image = new Image();
image.onload = (e) => {
const offscreenContext = canvas.getContext('2d');
canvas.width = image.width;
canvas.height = image.height;
offscreenContext.drawImage(image, 0, 0);
}
image.src = src;
};
※ offscreenCanvas는 web worker에서부터 기존 canvas와 같이 사용하면 됩니다.
위 로직은 2d context에 image를 draw
하는 방법입니다.
하지만 이 방법은 작동하지 않습니다.
이유는 web worker에서는 element(new Image)
를 만들 수 없기 때문이죠. 우리가 흔히 아는 document
객체가 없습니다.
그렇기 때문에 조금 다른 방식으로 image를 핸들링해야 합니다.
self.onmessage = async function (event) {
const { src, canvas } = event.data;
const response = await fetch(src);
const blob = await response.blob();
const imageBitmap = await createImageBitmap(blob);
const offscreenContext = canvas.getContext('2d');
canvas.width = imageBitmap.width;
canvas.height = imageBitmap.height;
offscreenContext.drawImage(imageBitmap, 0, 0);
};
이렇게 되면 web worker에서 이미지를 그리는 것이 가능합니다.
이제 사용법은 알았고, 어떤 상황에 유용한지 예제로 알아보겠습니다.
위 예제는 단순히 이미지를 렌더링하기 때문에 메인스레드가 차단될 일이 없습니다.
스레드에 부하를 주기 위해 복잡한 연산을 추가하겠습니다.
worker.js
self.onmessage = async function (event) {
const { src, canvas } = event.data;
const response = await fetch(src);
const blob = await response.blob();
const imageBitmap = await createImageBitmap(blob);
const offscreenContext = canvas.getContext('2d');
canvas.width = imageBitmap.width;
canvas.height = imageBitmap.height;
//복잡한 연산
const time = new Date();
while (new Date() - time <= 10000) {}
offscreenContext.drawImage(imageBitmap, 0, 0);
};
이렇게 되면 복잡한 연산을 10초간 수행해야 이미지가 그려집니다.
지금 이 로직이 web worker가 아닌 메인 스레드에서 실행되었다면, 10초동안 유저는 아무것도 하지 못하겠죠.
web worker를 사용하면 백그라운드 스레드에 10초간 부하를 주는 것이기 때문에, 유저는 기다리는 동안 다른 UI와 상호작용이 가능합니다.
이런식으로 offscreenCanvas와 web worker를 활용하면 아무리 무거운 계산이 있어도 메인스레드 차단없이 원활하게 서비스할 수 있게 됩니다.
canvas가 아니더라도 무거운 계산으로 인해 메인 스레드의 블로킹이 관찰된다면 web worker를 활용해보길 추천드립니다.
추가적으로
web worker
와main thread
간의 데이터 전송 비용도 있기 때문에, 단순한 계산일 경우 오히려 성능저하를 초래할 수도 있습니다.
퍼포먼스를 측정하여web worker
를 도입해야하는지 면밀히 검토하는 것이 좋습니다.