brunch

Web Worker, 브라우저에서 멀티스레드로 성능향상

by 아이나비시스템즈
맵스플랫폼 사업팀_브런치 (23).png



Ep.23


Web Worker란?


Web Worker는 자바스크립트에서 멀티 스레드를 사용할 수 있게 도입된 기술입니다. 기본적으로 브라우저에서 자바스크립트는 Call Stack, Web API, Callback Queue, MicrotaskQueue를 기반으로 Event Loop를 통해 코드를 실행합니다.


<참고>

멀티 스레드 : 자바스크립트는 싱글 스레드 언어이지만, 멀티 스레드를 사용하면 여러 작업들을 한 번에 동시에 처리 가능합니다.

Call Stack : 함수의 실행 컨텍스트가 쌓이는 LIFO(후입 선출) 구조로 쌓이는 공간입니다.

Web API : setTimeout, DOM 이벤트 등 비동기 API로 호출 시 브라우저에서 별도로 처리하여 Callback Queue로 보냅니다.

Callback Queue (Task Queue) : Web API가 완료 후 Call Stack으로 보내지기 전에 대기하는 공간입니다.

Microtask Queue : Promise, MutationObserver 등의 높은 우선순위의 작업을 대기하는 공간이며 Callback Queue보다 먼저 Call Stack으로 보내집니다.

Event Loop : Call Stack이 비어 있으면 Microtask를 우선적으로 Call Stack에 올려 실행하고, 그 뒤에 Callback Queue에서 Call Stack에 올립니다.


1. Javascript 실행 구조

사진1Javascript 실행 구조.gif 출처 : JavaScript Visualized: Event Loop, Web APIs, (Micro)task Queue


setTimeout(..., 2000) 호출 → 브라우저 Web API에 타이머 시작 요청 → Callback Queue로 등록 대기

setTimeout(..., 100) 호출 → 동일 과정을 거쳐 별도 타이머 등록

console.log('End of script') 호출 → 콘솔에 ‘End of script’ 출력

Call Stack이 비어 있으므로 Event Loop 동작

100ms 타이머 만료 → 콜백이 Callback Queue로 이동

Event Loop가 콜백을 Call Stack에 올려 실행 → console.log('100ms')

2000ms 타이머 만료 → 콜백이 Callback Queue로 이동

Event Loop가 콜백을 Call Stack에 올려 실행 → console.log('2000ms')

이러한 원리로 동작하는데 Call Stack에서의 작업이 길어지면 CallBack Queue의 작업들이 밀리게 됩니다.

그러면 페이지 렌더링이나 입력처리에 사용되는 DOM 이벤트가 CallBack Queue에서 대기하게 되어 사용자는 화면이 멈추거나 지연되는 현상을 겪게 됩니다.


2. 과도한 연산으로 인한 Freezing

출처 : https://codepen.io/tnrfqdth-the-solid/pen/zxxgamg


스피너는 js에서 setInterval로 각 프레임마다 회전 애니메이션을 수행하고, “계산 시작” 버튼을 눌러 계산을 시작하면 무거운 루프 연산으로 인해 UI가 멈추는 현상이 발생합니다.


setInterval 콜백이 16ms마다 Call Stack에 쌓여 회전 각도를 업데이트합니다.

버튼 클릭 시 시작되는 for 루프 연산이 Call Stack을 완전히 점유하여, 이후 등록된 setInterval 콜백이 실행되지 못합니다.

결과적으로 스피너 애니메이션이 중단되어 화면이 프리징됩니다.


이 문제를 해결하기 위해 도입된 Web Worker는 메인 스레드와 분리된 백그라운드 스레드에서 작업을 실행할 수 있게 해줍니다. 메인 스레드는 Web Worker에게 연산 작업을 위임하고, 처리된 결과만 Callback Queue로 전달받아 UI 렌더링과 이벤트 처리에 전념할 수 있습니다. 이를 통해 메인 스레드의 부담을 줄이고 사용자에게 끊기지 않는 UI/UX를 제공할 수 있습니다.


3. 워커의 사용 유무에 따른 비교

출처 : https://codepen.io/tnrfqdth-the-solid/pen/myyNpjb


워커 사용 유무에 따라 과도한 연산으로 프리징되는 현상과 워커를 사용함에 따라 끊기지 않는 UI/UX를 시각적으로 보여줍니다.


setInterval 콜백이 두 스피너(spinner-no, spinner-with)가 16ms마다 회전을 시도합니다.

워커 미사용 영역 : 버튼 클릭 시 메인 스레드에서 for 루프 계산이 실행되어 spinner-no 애니메이션이 중단됩니다.

워커 사용 영역 : 버튼 클릭 시 계산 로직을 Web Worker에 위임하여 spinner-with 애니메이션이 계속 부드럽게 회전합니다.

Web Worker가 계산 완료 후 postMessage로 결과를 전송하면, 메인 스레드 onmessage에서 워커 사용 영역의status-with에 결과를 표시합니다.



Web Worker의 작동 방식


아래 코드는 MDN에서 제공하는 기본적인 웹 워커 예제입니다. js는 main.js와 worker.js로 구성되어 있으며, 메인 스레드와 워커 간 메시지 흐름을 보여줍니다.


<출처 : 기본적인 워커 예제>


// main.js

const first = document.querySelector("#number1");

const second = document.querySelector("#number2");

const result = document.querySelector(".result");


if (window.Worker) {

// 1. 워커 생성

const myWorker = new Worker("worker.js");


// 2. 메시지 전송 (메인 -> 워커)

[first, second].forEach((input) => {

input.onchange = () => {

myWorker.postMessage([first.value, second.value]);

console.log("Message posted to worker");

};

});


// 5. 메시지 수신

myWorker.onmessage = (e) => {

result.textContent = e.data;

console.log("Message received from worker");

};

} else {

console.log("Your browser doesn't support web workers.");

}



// worker.js

// 3. 워커 내 처리

onmessage = (e) => {

console.log("Worker: Message received from main script");


const [a, b] = e.data;

const result = a * b;


// 4. 결과 전송 (워커-> 메인)

if (isNaN(result)) {

postMessage("Please write two numbers");

} else {

const workerResult = "Result: " + result;

console.log("Worker: Posting message back to main script");

postMessage(workerResult);

}

};



<단계별 상세 설명>


워커 생성 (const myWorker = new Worker('worker.js');)

- 메인 스레드는 worker.js를 별도 스레드에서 로드하고 초기화합니다.

메시지 전송 (메인 → 워커)

- 두 개의 입력 요소 값이 변경될 때마다 myWorker.postMessage([first.value, second.value])로 워커에 배열을 전송합니다.

워커 내 처리

- 워커의 onmessage 이벤트에서 전달된 두 값을 받아 곱셈 작업을 수행하고 결과 문자열을 생성합니다.

결과 전송 (워커 → 메인)

- 워커 내부에서 postMessage(workerResult)로 연산 결과를 메인 스레드로 전송합니다.

메시지 수신 (메인)

- 메인 스레드의 myWorker.onmessage 이벤트에서 결과를 수신하여 .result 요소에 표시합니다。



Web Worker의 활용 예시


이미지 처리 : 브라우저에서 대용량 이미지 필터링이나 리사이징 같은 무거운 픽셀 연산을 워커로 처리하여 메인 스레드의 렌더 성능 저하를 방지합니다.

데이터 파싱 및 변환 : CSV, JSON, XML 등 대규모 데이터 스트림을 워커에서 병렬로 파싱·변환한 뒤 필요한 부분만 메인 스레드로 전달해 UI 정체 없이 데이터 처리할 수 있습니다.

암호화/복호화 작업 : AES, RSA 같은 복잡하고 반복적인 암호화 알고리즘을 워커에서 실행하여 메인 스레드의 응답 성능을 유지합니다.

머신 러닝 추론 : TensorFlow.js 등 경량 머신 러닝 모델의 추론 작업을 워커에서 수행해 60fps 이상을 목표로 하는 실시간 UI가 끊기지 않도록 돕습니다.

게임 로직 및 물리 엔진 : 물리 계산, AI 경로 탐색 등 반복 연산이 많은 게임 로직을 워커로 분리해 메인 스레드에서 부드러운 렌더링과 사용자 입력 처리를 가능하게 합니다。

기타 : 그 밖에도 UI에 직접 관여하지 않으면서 연산이 복잡하거나 많은 로직을 워커로 이전하여 처리함으로써 성능 개선을 이룰 수 있습니다.



Web Worker의 제약 사항


DOM 접근 불가 : 워커 스코프에서는 document, window 등 DOM API를 직접 사용할 수 없으며 그렇기에 UI를 직접 업데이트하거나 프론트에서 직접 값을 가져올 수 없기 때문에 워커 내에서의 작업은 순수한 연산과 데이터 처리로 한정됩니다.

스레드 생성 비용 : 워커를 생성하고 메시지를 전달하는 행위 자체에 비용이 발생하고 워커마다 별도의 메모리공간을 차지하고 있습니다. 그러므로 너무 많은 워커를 만들거나 아주 작은 작업까지 처리하려고 할 시 오히려 성능이 저하될 수 있습니다.

데이터 복사 비용 : 메인 스레드와 워커 간 데이터 교환 시 직접 공유하는 것이 아닌 데이터 복사가 발생하므로 큰 데이터를 주고받을 때 지연이 생길 수 있습니다.

디버깅 어려움 : 브라우저 DevTools에서 워커 스크립트 디버깅이 제한적이며, console 출력도 메인 페이지 콘솔과 분리되어 동작하므로 어려움이 있습니다.

지원 호환성 : 구형 브라우저(특히 IE)나 일부 모바일 환경에서 Web Worker 지원이 불완전할 수 있습니다.


by 아이나비시스템즈 지도기술개발팀


iMPS배너_최종_하얀색.png

국내 가격 경쟁력 1위! 아이나비가 만든 차별화된 지도 솔루션, iMPS 바로가기


#webworker #자바스크립트 #멀티스레드 #callstack #mdn #지도 #아이나비시스템즈

keyword
작가의 이전글협력형 자율주행 제어 시스템 아키텍처의 이해