자바스크립트로 개발을 하면서 복잡한 계산 처리나 오래 걸리는 작업을 처리할 일이 생겼다. 하지만 싱글 스레드 기반으로 동작하는 자바스크립트는 한계가 있었다. 자바스크립트는 싱글스레드 제약을 극복하고 병렬처리가 가능하도록 못하는 걸까 ? 🤔 효율적인 방법을 찾던 중에 웹 워커라는 것을 알게 되었다. 오늘은 웹 워커에 대해서 공부한 내용과 적용한 코드를 보면서 공부해보려고 한다.
싱글스레드 방식
자바스크립트는 브라우저에서 싱글스레드 기반으로 동작한다. 따라서 한번에 하나의 작업만을 처리할 수 있다. 스택이 하나밖에 없다. 그렇기 때문에 자바스크립트는 싱글스레드언어(== 한번에 코드 한줄 밖에 실행 못함)라고 한다.
메인 스레드는 UI 스레드라고도 불리는데 이는 메인 스레드가 주로 사용자의 상호 작용, 이벤트 처리, DOM 조작 등이 주로 메인 스레드에서 수행되기 때문이다.
자바스크립트는 코드 라인을 병렬로 실행할 수 없지만 여러 작업을 동시에 처리할 수 있는 이벤트 루프 방식이 있다.
코드가 실행되면 한 태스크씩 스택에 쌓이게 된다. 하지만 비동기 작업이나 이벤트 리스너는 먼저 큐에 순차적으로 들어간다. 이벤트 루프가 이를 모니터링 하고 있다가 스택이 비어 있을때 큐에 있는 처리가 완료 된 코드들이 스택으로 이동시킨다. 이 프로세스를 통해 비동기적인 작업이 동작하면서도 메인 스레드가 차단되지 않고 다른 코드를 실행할 수 있게 된다.
이벤트 루프 사용하면 되는거 아냐 ? 웹 워커를 사용하는 이유는 뭐야 ?
이벤트 루프와 웹 워커는 둘 다 자바스크립트에서 비동기적인 작업을 다루기 위한 메커니즘이지만 웹 워커를 사용하는 이유는 주로 성능 향상과 작업들을 효율적으로 처리하기 위함이다.
데이터 처리가 많아질수록 병목현상이 생겨 UI 업데이트가 지연되고 사용자에게 느리게 느껴지게 된다. 이를 해결하기 위해 브라우저에서 Web Worker API를 제공하고 있다.
- 병렬 처리로 인한 성능 향상
이벤트 루프는 메인스레드에서만 실행되지만 웹 워커는 별도의 백그라운드 스레드에서 실행되므로 병렬 처리를 통해 성능을 향상시킬 수 있다.
특히, 계산이 많이 필요한 작업이나 긴 지속 시간의 작업을 별도의 스레드에서 처리함으로써 메인 스레드가 다른 작업에 집중할 수 있게 된다.
- 메인스레드 성능 개선
메인 스레드(==UI 스레드)와 비교했을 때, 웹 워커에는 DOM과 상호 작용할 수 없다. 웹 워커는 복잡한 계산 또는 긴 시간이 소요되는 작업을 담당하므로, 메인 스레드는 주로 UI 갱신, 사용자 입력 처리 등에 집중할 수 있다.
- 복잡한 계산 및 데이터 처리
웹 워커는 일반적으로 계산이 많거나 시간이 오래 걸리는 작업을 메인 스레드와 분리하여 성능을 향상시키는 데 사용된다.
대용량의 데이터를 가공하거나 복잡한 알고리즘을 실행하는 작업을 웹 워커를 활용하여 메인 스레드의 부담을 줄일 수 있다.
Web Work 사용해보기
간단하게 웹워커를 사용한것과 사용하지 않은 것을 구현해보며 비교해보자.

일단 두개의 버튼을 만들어 보았다. count+1
버튼을 누르면 count가 1씩 증가되고 , answer+100
버튼을 누르면 answer이 100씩 증가한다.
하지만 answer+100
의 버튼을 누르면 500000000번을 돌리는 복잡한 함수가 실행된다.
Non worker
웹워커를 사용하지 않았을 경우 돌려보자.
import React, { useState } from "react";
export const NonWorker = () => {
const [count, setCount] = useState(0);
const [answer, setAnswer] = useState(0);
const getAnswer = () => {
let sum = 0;
for (let i = 0; i < 500000000; i++) {
sum += i;
}
setAnswer((prev) => prev + 100);
};
return (
<div className="box-wrap">
<div className="text-wrap">count : {count}</div>
<div className="text-wrap">answer : {answer}</div>
<button
className="custom-button"
onClick={() => setCount((prev) => prev + 1)}
>
count + 1
</button>
<button className="custom-button" onClick={getAnswer}>
answer + 100
</button>
</div>
);
};

answer+100
버튼을 누르자 500000000번의 루프가 다 돌때까지 coun+1의 버튼을 마구 눌러도, UI의 반응이 없었다. 그리고 루프가 끝나면 count의 값이 한번에 바뀌는 현상이 나타났다.
이벤트 루프를 살펴보자.

getAnswer함수가 오랜시간 걸리기때문에, 이벤트 루프에서는 이 함수가 끝날 때 까지 다른 이벤트의 작업을 처리하지 못하고 큐에 쌓이게 된다. 즉, 이 함수의 작업이 끝날 때까지 메인 스레드는 다른 이벤트나 작업을 처리하지 못하고 기다려서 UI 업데이트나 이벤트 응답과 같은 작업들이 블로킹된것.
Web Worker
Web Worker는 필요한 개수만큼 생성할 수 있고 스레드를 만들어 사용하는 것과 같다.
일단 오래 걸리는 함수를 worker.js에 따로 옮겨주자.
// utils/worker.js
onmessage = (event) => {
if (event.data === "start") {
let sum = 0;
for (let i = 0; i < 500000000; i++) {
sum += i;
}
console.log(sum);
postMessage(100);
}
};
import React, { useState } from "react";
const url = new URL("../utils/worker.js", import.meta.url);
const worker = new Worker(url);
export const WebWorker = () => {
const [count, setCount] = useState(0);
const [answer, setAnswer] = useState(0);
const getAnswer = () => {
worker.postMessage("start");
worker.onmessage = (event) => setAnswer((prev) => prev + event.data);
};
return (
<div className="box-wrap">
<div className="text-wrap">count : {count}</div>
<div className="text-wrap">answer : {answer}</div>
<button
className="custom-button"
onClick={() => setCount((prev) => prev + 1)}
>
count + 1
</button>
<button className="custom-button" onClick={getAnswer}>
answer + 100
</button>
</div>
);
};
Worker() 생성자를 호출하여 워커 스레드에서 실행할 스크립트의 경로를 지정하면면 된다. 이때, Webpack에서 Worker API를 사용할 때는 Worker 생성과 URL 생성을 모두 인라인으로 작성해야 한다. ( 이 부분에서 삽질을 꽤 했다.. ㅠ_ㅜ)
메인 스레드와 Web Worker는 메시지 방식으로 서로 통신하며 데이터를 주고받는다. postMessage() 메서드와 onmessage 이벤트 핸들러를 통해 메인스레드와 워커는 메세지로 주고받을 수 있다.
worker.postMessage("start")
: 메인 스레드는 워커에게 "start" 메시지를 보낸다.worker.onmessage = (event) => setAnswer((prev) => prev + event.data)
: 웹워커로 부터 메세지를 받아 메인 스레드의 상태를 업데이트한다.

answer + 100 버튼을 눌러도 count+1 버튼이 눌리면서 UI또한 즉각적으로 바뀌는것을 볼 수 있다.
이벤트 루프를 살펴보자.


answer+100 버튼을 눌렀을 때, WebWork API에게 메세지를 보내고 스택은 비워지게 된다.
그리고 이벤트 루프는 새로들어온 클릭이벤트를 스택으로 올려서 바로바로 처리하고 (이 때문에 즉각적인 반응이 가능한것!) WebWork가 응답이 완료되면 큐에 올리고 스택이 비워지면 다시 스택으로 올린다. 스택의 일이 다 처리면되 UI가 업데이트 된다!
참고 자료
https://tech.kakao.com/2021/09/02/web-worker/