


브라우저는 기본적으로 단일 스레드를 사용하여 모든 JS 코드를 실행한다.
웹 환경에서 앱 처럼 멀티 스레딩 모델을 활용하기 위해 사용하는 것이 바로 웹 워커.

기본이라고 적혀있는 것이 바로 메인 스레드이고, Worker 가 바로 Worker 스레드의 작업이다.
메인 스레드가 fetch, UI 렌더링, 사용자 이벤트 핸들링 등 여러 작업으로
Busy 하더라도, Worker 스레드를 활용하여 다른 작업을 수행할 수 있다.
첫째로 DOM 조작이 직접적으로 불가능하다.
백그라운드에서 실행되기 때문에 DOM 요소에 직접 접근할 수 없으며,
postMessage() 로 데이터를 전달하여 메인 스레드에서 처리해야한다.
두번째로 전송비용과 추가적인 메모리 사용이다.
두 스레드간 postMsg, onMsg로 결과물들을 주고 받는 과정이 있다.
이 과정이 너무 자주 반복된다면 오히려 오버헤드가 발생하여 웹 프로덕트의 반응성이 떨어질 수 있다.
우리 프로젝트에서 뒤에 배경에 400개의 별을 그려서 사용자의 클릭을 받아서
이동하는 효과가 있다.
이때 별 객채 생성과 위치 계산을 worker로 맡기고,
메인 스레드는 이 결과를 렌더링만 하는 방식으로 적용하였다.
self.onmessage = (e: MessageEvent<WorkerMessage>) => {
const {
type,
canvasWidth: width,
canvasHeight: height,
viewPos,
maxStars: starsCount,
} = e.data;
switch (type) {
case 'init':
if (width && height) {
canvasWidth = width;
canvasHeight = height;
maxStars = starsCount || 400;
// 별들 초기화
stars = [];
for (let i = 0; i < maxStars; i++) {
stars.push(createStar());
}
self.postMessage({
type: 'initialized',
starCount: stars.length,
});
}
break;
case 'update':
if (viewPos) {
// 별들 업데이트
updateStars();
// 위치 정보 전송
const positions = getStarPositions(viewPos);
self.postMessage({
type: 'positions',
positions,
});
}
break;
case 'resize':
if (width && height) {
canvasWidth = width;
canvasHeight = height;
// 별들 재생성
stars = [];
for (let i = 0; i < maxStars; i++) {
stars.push(createStar());
}
self.postMessage({
type: 'resized',
starCount: stars.length,
});
}
break;
}
};
workerRef.current = new Worker(
new URL('../../workers/starfield.worker.ts', import.meta.url),
{
type: 'module',
}
);
workerRef.current.onmessage = (e) => {
const { type, starCount, positions } = e.data;
switch (type) {
case 'worker_ready':
console.log('Starfield Worker 준비됨');
// Worker 초기화
workerRef.current?.postMessage({
type: 'init',
canvasWidth: canvas.width,
canvasHeight: canvas.height,
maxStars: 400,
});
break;
case 'initialized':
console.log(`Starfield Worker 초기화 완료: ${starCount}개 별 생성`);
break;
case 'positions':
starPositionsRef.current = positions;
break;
case 'resized':
console.log(
`Starfield Worker 리사이즈 완료: ${starCount}개 별 재생성`
);
break;
}
};
이렇게 PostMessage와 OnMessage를 통해서 Star 객체의 생성, 위치 계산 결과를 주고
화면에 렌더링하는 로직을 Web Worker를 활용하여 메인 스레드의 부담을 줄이고자 노력하였다.
두번째 단점때문에 적용하지 못했다.
일단 우리 프로젝트는 캔버스를 1 픽셀씩 채우는 프로젝트이다.
사용자는 캔버스를 확대/축소 하고, 픽셀을 선택하면 그 픽셀이 ViewPort 중심에 오도록
캔버스가 이동하는 효과까지 제공한다.
따라서 캔버스의 width, height 의 재계산이 빈번하다.
이렇게 ResizeObserver를 통해 캔버스의 크기를 재계산 한다.
const observer = new ResizeObserver((entries) => {
const { width, height } = entries[0].contentRect;
if (width === 0 || height === 0) return;
[renderCanvasRef.current, previewCanvasRef.current, interactionCanvasRef.current]
.forEach((canvas) => {
if (canvas) {
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.round(width * dpr); // 실시간 크기 변경
canvas.height = Math.round(height * dpr);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
canvas.getContext('2d')?.scale(dpr, dpr);
}
});
});
즉 크기가 자주 바뀌는 Interaction, Preview, ImageOverlay, Render 에도
web worker를 적용하게 되면, 크기가 바뀔때마다 PostMessage <-> OnMessage 과정을
자주 거치게 된다.
사실 이를 생각하지 않고 무지성으로 적용해보니,
캔버스에 확대,축소,시점이동 하려고 하니까 계속 깜빡이면서 제대로 동작하지 않는 것을 확인했다.
따라서 width, height의 변화가 적은 배경을 렌더링하는 Canvas에만 적용하였다.