웹 워커는 **"진짜 멀티스레딩(Real Multi-threading)"**이다.
많은 사람들이 "자바스크립트는 싱글 스레드다"라고 알고 있는데, 정확히는 **"자바스크립트 엔진(V8 등)의 메인 이벤트 루프가 싱글 스레드"**다. 웹 워커는 브라우저 수준에서 이 제약을 넘어서게 해준다.
1. 싱글 스레드 vs 웹 워커 (OS 관점)
■ 일반적인 자바스크립트 (Single Thread)
- 우리가 작성하는 일반적인 코드는 Main Thread 하나에서 실행
- UI 렌더링, 이벤트 리스너(클릭 등), HTTP 요청 처리가 모두 이 스레드 하나에서 줄을 서서 대기
- 만약 for문을 10억 번 돌리면, 그동안 브라우저는 클릭도 못 받고 화면도 멈춤(Blocking).
■ 웹 워커를 쓴 경우 (Multi-threading)
- 웹 워커를 생성하면 브라우저는 OS(운영체제)에게 **"새로운 스레드를 하나 더 할당해줘"**라고 요청
- Main Thread: 여전히 UI를 그리고 사용자의 클릭 이벤트 추적 (60fps 유지)
- Worker Thread: 메인 스레드와 완전히 분리된 별도의 CPU 코어 자원을 점유하며 무거운 계산(거리 계산 등)을 수행
- 이 둘은 서로의 메모리를 공유하지 않고, postMessage라는 통로를 통해 데이터를 주고받을 뿐
2. "유사"가 아닌 "진짜"인 이유
웹 워커가 진짜 멀티스레딩인 이유는 병렬성(Parallelism) 때문
- 동시 실행: 메인 스레드에서 애니메이션이 돌아가는 "동시에", 워커 스레드에서 수학 계산이 물리적으로 다른 CPU 코어에서 실행
- 독립적 컨텍스트: 워커는 자신만의 스택과 힙 메모리를 가진다. 메인 스레드가 에러로 멈춰도 워커는 계속 돌아갈 수 있고, 반대의 경우도 마찬가지.
- OS 스레드 매핑: 최신 브라우저들은 웹 워커를 실제 운영체제의 시스템 스레드(System Thread)에 매핑하여 처리
3. 그런데 왜 자바스크립트를 싱글 스레드라고 부를까?
- 자바스크립트 언어 자체의 설계 철학 때문
- 공유 메모리 금지: 멀티스레딩의 가장 큰 골칫덩이는 여러 스레드가 동시에 같은 변수를 수정하려다 발생하는 'Race Condition'.
- 자바스크립트는 이를 원천 차단하기 위해 워커가 메인 스레드의 DOM에 직접 접근하거나 변수를 직접 수정하는 것을 금지
- 메시지 패싱: 데이터를 보낼 때도 원본을 주는 게 아니라 복사본을 만들어 전송(Structured Clone). "안전한 멀티스레딩"
4. 요약: 게임 개발에서의 체감
- 웹 워커 미사용: 오답 클릭 시 거리 계산($N$번) 때문에 아주 찰나의 순간(0.01~0.1초) 화면이 툭 끊김 → 고득점을 노리는 사용자에게 불쾌한 경험
- 웹 워커 사용: 클릭 즉시 반응. 계산은 백그라운드에서 조용히 처리 → 사용자는 멀티스레딩의 존재를 모르지만 부드러운 게임 플레이를 즐김
결국 웹 워커는 자바스크립트라는 언어의 싱글 스레드 특성을 유지하면서도, 하드웨어의 멀티코어 성능을 뽑아 쓸 수 있게 해주는 브라우저의 마법 같은 존재
예제
1. Worker 로직 작성 (public/workers/distance.worker.js)
먼저 복잡한 거리 계산을 담당할 워커 파일 생성. 이 파일은 메인 스레드와 별개로 작동
// public/workers/distance.worker.js
self.onmessage = (e) => {
const { clickCoord, remainingTargets } = e.data;
// 모든 남은 객체와의 거리 계산 (피타고라스 정리)
const distances = remainingTargets.map((target) => {
const dx = target.x - clickCoord.x;
const dy = target.y - clickCoord.y;
const distance = Math.sqrt(dx * dx + dy * dy);
return {
target_id: target.id,
distance: parseFloat(distance.toFixed(2)), // 소수점 2자리까지
};
});
// 계산 완료 후 메인 스레드로 결과 전송
self.postMessage(distances);
};
2. Next.js 클라이언트 컴포넌트 (components/GameCanvas.tsx)
워커를 생성하고 클릭 이벤트를 처리하는 부분
'use client';
import { useEffect, useRef, useState } from 'react';
export default function GameCanvas() {
const workerRef = useRef<Worker>();
const [logs, setLogs] = useState<any[]>([]);
const [remainingTargets, setRemainingTargets] = useState([
{ id: 'tree_01', x: 100, y: 200 },
{ id: 'bird_02', x: 450, y: 150 },
// ... 더 많은 객체들
]);
useEffect(() => {
// 1. 워커 초기화
workerRef.current = new Worker(new URL('../public/workers/distance.worker.js', import.meta.url));
// 2. 워커로부터 계산된 데이터를 받았을 때의 처리
workerRef.current.onmessage = (e: MessageEvent) => {
const distances = e.data;
// 기존 로그 리스트의 마지막 항목에 거리 데이터 업데이트 (함수형 업데이트 사용)
setLogs((prev) => {
const newLogs = [...prev];
if (newLogs.length > 0) {
newLogs[newLogs.length - 1].distances_to_remaining_targets = distances;
}
return newLogs;
});
console.log('거리 계산 완료:', distances);
};
return () => workerRef.current?.terminate(); // 컴포넌트 언마운트 시 워커 종료
}, []);
const handleClick = (e: React.MouseEvent) => {
const x = e.clientX;
const y = e.clientY;
const playTime = 50000; // 예시 실행 시간
// 새로운 로그 객체 생성 (거리는 아직 비어있음)
const newLog = {
user_id: "user_123",
game_id: "map_01",
event_id: crypto.randomUUID(),
click_coordinate: { x, y },
play_time_ms: playTime,
result_type: "MISS", // 실제 로직에선 정답 체크 함수 호출
distances_to_remaining_targets: []
};
setLogs((prev) => [...prev, newLog]);
// 3. 워커에게 무거운 계산 작업 위임
workerRef.current?.postMessage({
clickCoord: { x, y },
remainingTargets: remainingTargets
});
};
const handleGameEnd = async () => {
// 게임 종료 시 FastAPI로 로그 전송
try {
const response = await fetch('https://your-fastapi-server.com/logs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ logs }),
});
if (response.ok) alert('로그 전송 완료!');
} catch (error) {
console.error('전송 실패', error);
}
};
return (
<div
onClick={handleClick}
style={{ width: '100vw', height: '100vh', background: '#eee' }}
>
<h1>게임 화면 (클릭해보세요)</h1>
<button onClick={handleGameEnd}>게임 종료 및 데이터 전송</button>
</div>
);
}
💡 코드 포인트
-
Non-blocking UI: handleClick 함수 내에서 setLogs로 클릭 지점은 즉시 기록하지만, 실제 Math.sqrt가 반복되는 루프 연산은 workerRef.current.postMessage를 통해 백그라운드에서 진행. 따라서 객체가 1000개여도 화면은 멈추지 않음
-
crypto.randomUUID(): 각 로그를 구분하기 위한 고유 ID를 생성
-
FastAPI 연동: handleGameEnd 함수에서 수집된 logs 배열을 한꺼번에 JSON으로 직렬화하여 서버로 전송
-
terminate(): 워커는 리소스를 사용하므로 페이지를 벗어날 때 반드시 종료
웹 워커는 **"진짜 멀티스레딩(Real Multi-threading)"**이다.
많은 사람들이 "자바스크립트는 싱글 스레드다"라고 알고 있는데, 정확히는 **"자바스크립트 엔진(V8 등)의 메인 이벤트 루프가 싱글 스레드"**다. 웹 워커는 브라우저 수준에서 이 제약을 넘어서게 해준다.
1. 싱글 스레드 vs 웹 워커 (OS 관점)
■ 일반적인 자바스크립트 (Single Thread)
■ 웹 워커를 쓴 경우 (Multi-threading)
2. "유사"가 아닌 "진짜"인 이유
웹 워커가 진짜 멀티스레딩인 이유는 병렬성(Parallelism) 때문
3. 그런데 왜 자바스크립트를 싱글 스레드라고 부를까?
4. 요약: 게임 개발에서의 체감
결국 웹 워커는 자바스크립트라는 언어의 싱글 스레드 특성을 유지하면서도, 하드웨어의 멀티코어 성능을 뽑아 쓸 수 있게 해주는 브라우저의 마법 같은 존재
예제
1. Worker 로직 작성 (public/workers/distance.worker.js)
먼저 복잡한 거리 계산을 담당할 워커 파일 생성. 이 파일은 메인 스레드와 별개로 작동
2. Next.js 클라이언트 컴포넌트 (components/GameCanvas.tsx)
워커를 생성하고 클릭 이벤트를 처리하는 부분
💡 코드 포인트
Non-blocking UI: handleClick 함수 내에서 setLogs로 클릭 지점은 즉시 기록하지만, 실제 Math.sqrt가 반복되는 루프 연산은 workerRef.current.postMessage를 통해 백그라운드에서 진행. 따라서 객체가 1000개여도 화면은 멈추지 않음
crypto.randomUUID(): 각 로그를 구분하기 위한 고유 ID를 생성
FastAPI 연동: handleGameEnd 함수에서 수집된 logs 배열을 한꺼번에 JSON으로 직렬화하여 서버로 전송
terminate(): 워커는 리소스를 사용하므로 페이지를 벗어날 때 반드시 종료