게임 로직을 위한 Web Workers
Web Workers를 쓰면 JavaScript를 백그라운드 스레드에서 실행할 수 있습니다. 게임에서는 물리, AI, 절차적 생성을 프레임 드롭 없이 돌릴 수 있다는 뜻입니다.
1) Worker를 언제 쓸까
잘 맞는 경우:
- 물리 시뮬레이션
- 경로 탐색 (A*, navmesh)
- AI 의사 결정
- 절차적 생성
- 에셋 처리 (이미지 가공, 압축)
- 복잡한 수학 (FFT, 충돌 감지)
잘 안 맞는 경우:
- 렌더링 (Worker는 DOM/Canvas에 직접 접근할 수 없습니다)
- 아주 작은 작업 (스레드 오버헤드가 더 큽니다)
2) 기본 Worker 만들기
worker.js:
js
self.onmessage = (e) => {
const { type, data } = e.data
if (type === 'calculate') {
const result = heavyCalculation(data)
self.postMessage({ type: 'result', data: result })
}
}
function heavyCalculation(input) {
// 무거운 작업은 여기서
return input * 2
}main.js:
js
const worker = new Worker('worker.js')
worker.onmessage = (e) => {
const { type, data } = e.data
if (type === 'result') {
console.log('Got result:', data)
}
}
worker.postMessage({ type: 'calculate', data: 42 })3) 인라인 Worker (별도 파일 없이)
js
function createInlineWorker(fn) {
const blob = new Blob([`(${fn.toString()})()`], { type: 'text/javascript' })
return new Worker(URL.createObjectURL(blob))
}
const worker = createInlineWorker(() => {
self.onmessage = (e) => {
const result = e.data * 2
self.postMessage(result)
}
})4) ES 모듈 Worker
Worker 코드를 ES 모듈로 작성하고 importScripts 대신 그 안에서 import를 쓸 수 있습니다. 생성자에 { type: 'module' }을 넘기면 됩니다:
js
const worker = new Worker('physics-worker.js', { type: 'module' })이렇게 하면 Worker 안에서도 메인 스레드에서 하던 것과 똑같이 공유 수학, 충돌, AI 헬퍼를 import할 수 있어서 게임 로직이 중복되지 않습니다. 모듈 Worker는 Chrome과 Edge 80+, Safari 15+, Firefox 114+에서 지원됩니다.
5) 물리 Worker 예제
physics-worker.js:
js
const bodies = []
const FIXED_DT = 1 / 60
self.onmessage = (e) => {
const { type, data } = e.data
switch (type) {
case 'init':
initWorld(data)
break
case 'step':
step()
break
case 'addBody':
bodies.push(data)
break
}
}
function initWorld(config) {
// 물리 월드 초기화
}
function step() {
// 물리 업데이트
for (const body of bodies) {
body.vy += 9.8 * FIXED_DT // 중력
body.x += body.vx * FIXED_DT
body.y += body.vy * FIXED_DT
}
// 위치를 다시 보내기
self.postMessage({
type: 'positions',
data: bodies.map(b => ({ id: b.id, x: b.x, y: b.y, rotation: b.rotation }))
})
}6) 성능을 위한 transferable 객체
큰 데이터는 복사 없이 전달할 수 있습니다:
js
// 메인 스레드
const positions = new Float32Array(1000)
worker.postMessage(positions, [positions.buffer])
// positions는 이제 여기서 쓸 수 없습니다 (전달됨)
// Worker
self.onmessage = (e) => {
const positions = e.data
// positions로 작업
self.postMessage(positions, [positions.buffer])
}7) 경로 탐색 Worker
pathfinding-worker.js:
js
let grid = null
self.onmessage = (e) => {
const { type, data } = e.data
if (type === 'setGrid') {
grid = data
}
if (type === 'findPath') {
const path = aStar(data.start, data.end, grid)
self.postMessage({ type: 'path', id: data.id, path })
}
}
function aStar(start, end, grid) {
// A* 구현
const openSet = [start]
const cameFrom = new Map()
const gScore = new Map()
gScore.set(key(start), 0)
while (openSet.length > 0) {
// ... A* 로직
}
return reconstructPath(cameFrom, end)
}
function key(pos) {
return `${pos.x},${pos.y}`
}8) 병렬 작업을 위한 Worker 풀
js
class WorkerPool {
constructor(workerUrl, size = navigator.hardwareConcurrency || 4) {
this.workers = []
this.queue = []
this.available = []
for (let i = 0; i < size; i++) {
const worker = new Worker(workerUrl)
worker.onmessage = (e) => this.handleResult(worker, e)
this.workers.push(worker)
this.available.push(worker)
}
}
run(data) {
return new Promise((resolve) => {
const task = { data, resolve }
if (this.available.length > 0) {
this.dispatch(this.available.pop(), task)
} else {
this.queue.push(task)
}
})
}
dispatch(worker, task) {
worker._currentTask = task
worker.postMessage(task.data)
}
handleResult(worker, e) {
const task = worker._currentTask
task.resolve(e.data)
if (this.queue.length > 0) {
this.dispatch(worker, this.queue.shift())
} else {
this.available.push(worker)
}
}
terminate() {
this.workers.forEach(w => w.terminate())
}
}9) 실시간 동기화를 위한 SharedArrayBuffer
교차 출처 격리(cross-origin isolation)를 켜면 메모리를 공유할 수 있습니다:
js
// 메인 스레드
const shared = new SharedArrayBuffer(1024)
const positions = new Float32Array(shared)
worker.postMessage({ type: 'init', buffer: shared })
// Worker가 공유 메모리에 직접 읽고 씁니다
// 위치 업데이트에 postMessage 오버헤드가 없습니다10) Worker용 OffscreenCanvas
Worker 안에서 렌더링하기. OffscreenCanvas는 이제 Chrome, Edge, Firefox, Safari 전반에서 Baseline Widely available 상태입니다 (Safari는 macOS와 iOS의 17.0에서 지원을 추가했습니다):
js
// 메인 스레드
const canvas = document.getElementById('game')
const offscreen = canvas.transferControlToOffscreen()
worker.postMessage({ canvas: offscreen }, [offscreen])
// Worker
self.onmessage = (e) => {
const canvas = e.data.canvas
const ctx = canvas.getContext('2d')
function render() {
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 그리기...
requestAnimationFrame(render)
}
render()
}11) 에러 처리
js
worker.onerror = (e) => {
console.error('Worker error:', e.message, e.filename, e.lineno)
}
// Worker 안에서
self.onerror = (e) => {
self.postMessage({ type: 'error', message: e.message })
}관련 글
- 빠르게 로드되는 웹 게임 출시하기
- Wasm 스레드 활성화하기 (SharedArrayBuffer)
- Canvas 2D 게임 루프
- 게임 물리 라이브러리 — Worker에서 Rapier나 Cannon-es 돌리기
- 스트리밍 에셋 로딩 — 메인 스레드 밖에서 에셋 디코딩하기
- 2026년 웹 게임 기술 스택 — Worker가 WebGL/WebGPU/Wasm 스택에 어떻게 들어맞는지
외부 자료
- MDN: Web Workers API — 전체 API 레퍼런스
- MDN: Using Web Workers — 단계별 가이드
- MDN: Transferable objects — 무복사 데이터 전달
- MDN: SharedArrayBuffer — 스레드 간 공유 메모리