React ์ฑ์ด ๋ฒ๋ฒ ๊ฑฐ๋ฆฌ๋ ์ง์ง ์ด์ : Web Workers๋ก ์ฑ๋ฅ ๊ฐ์ ํ๊ธฐ
React ์ฑ์ด ๋ฒ๋ฒ ๊ฑฐ๋ฆฌ๋ ์ง์ง ์ด์ : Web Workers๋ก ์ฑ๋ฅ ๊ฐ์ ํ๊ธฐ
์ด๋ฐ ๊ฒฝํ ๋ค๋ค ์์์์. ๋ฐ์ดํฐ ์ฒ๋ฆฌํ๋ ๊ธฐ๋ฅ ๋ง๋๋๋ฐ, ๋์ฉ๋ ๋ฆฌ์คํธ ํํฐ๋ง์ด๋ CSV ํ์ฑ์ด๋ ๋ณต์กํ ๊ณ์ฐ์ด๋ . ๊ฐ๋ฐ ํ๊ฒฝ์์ 100๊ฑด์ผ๋ก ํ ์คํธํ๋ฉด ์ฐฐ๋ก์ด์์. ๊ทผ๋ฐ ํ๋ก๋์ ์์ 50,000๊ฑด ๋ค์ด์ค๋๊น UI๊ฐ 3์ด ๋์ ์ผ์ด๋ฒ๋ฆผ. ์ ์ ๊ฐ ๋ฒํผ ํด๋ฆญํด์. ์๋ฌด ๋ฐ์ ์์. ๋ ํด๋ฆญํด์. ์ฌ์ ํ ์์. ๊ทธ๋ฌ๋ค ๊ฐ์๊ธฐ ํ๊บผ๋ฒ์ ๋ค ํฐ์ ธ์.
๋ฉ์ธ ์ค๋ ๋ ํฐ์ง๊ธฐ ์ง์ ์ด๊ณ , ์ ์ ๋ค์ ์ดํ ์ค์ด์์.
JavaScript์ ๋ผ์ํ ํ์ค์ด์์. ์ฑ๊ธ ์ค๋ ๋๋ผ์์. ๋ชจ๋ ์ ๋๋ฉ์ด์ , ํด๋ฆญ ํธ๋ค๋ฌ, API ์๋ต, ๋ณต์กํ ๊ณ์ฐ์ด ๊ทธ ํ๋๋ฟ์ธ ์ค๋ ๋์์ ์๋ฆฌ์ธ์์ ํด์. ๋ฌด๊ฑฐ์ด ์์ ์ด ์ค๋ ๋๋ฅผ ์ ๋ นํ๋ฉด, ๊ณต๋ค์ฌ ๋ง๋ 60fps UI๊ฐ 0fps๋ก ๋ ๋จ์ด์ ธ์. ๋ธ๋ผ์ฐ์ ๊ฐ ํ๋ ์๋ ๋ชป ๊ทธ๋ฆฌ๊ณ ์ ๋ ฅ์ ๋ฐ์๋ ๋ชป ํด์.
ํด๊ฒฐ์ฑ ? Web Workers์์. ๋น์ผ ์์ ์ ๋ฉ์ธ ์ค๋ ๋์์ ์์ ํ ๋นผ๋ฒ๋ ค์. UI ์ค๋ ๋๋ ๋ ๋๋ง์ด๋ ์ ์ ๋ฐ์์ ์ง์คํ๊ฒ ํ๊ณ , ๋ฐฑ๊ทธ๋ผ์ด๋ ์ค๋ ๋๊ฐ ๋ณ๋ ฌ๋ก ์ซ์ ๊ณ์ฐํ๋ฉด ๋ผ์.
๊ทผ๋ฐ ๋ฌธ์ ๊ฐ ํ๋ ์์ด์. ๋๋ถ๋ถ์ Web Worker ํํ ๋ฆฌ์ผ์ด "์ซ์ ๋ ๊ฐ ๋ํ๊ธฐ" ์์ค ์์ ๋ณด์ฌ์ฃผ๊ณ ๋์ด๊ฑฐ๋ ์. ์ค์ React ์ฑ์์ Worker ์ฐ๋ ค๊ณ ํ๋ฉด ๋งํ๋ ๊ฒ ํ๋์ด ์๋์์:
- ๋ณต์กํ ๋ฐ์ดํฐ ๊ตฌ์กฐ๋ ์ด๋ป๊ฒ ๊ณต์ ํ์ง?
- TypeScript ํ์ ์?
- ์๋ฌ ์ฒ๋ฆฌ๋ ์ด๋ป๊ฒ ์ ๋๋ก ํ์ง?
- Worker์์ npm ํจํค์ง ์ธ ์ ์์ด?
- ๋ด ์ฑ์ ๋ง๋ ์ํคํ ์ฒ๋ ๋ญ์ง?
์ด ๊ธ์์ ๋ค ๋ค๋ค๋ณผ๊ฒ์. ์ ๋๋ก ํ๋ด ์๋ค.
๋ฉ์ธ ์ค๋ ๋ ๋ฌธ์ ์ ๋๋ก ์ดํดํ๊ธฐ
๋ฌธ์ ๋ฅผ ๊ณ ์น๋ ค๋ฉด ๋จผ์ ์ดํดํด์ผ์ฃ . JavaScript๋ "๋ฉ์ธ ์ค๋ ๋"๋ผ๋ ๋จ์ผ ์ค๋ ๋์์ ๋์๊ฐ์. ์ด ์ค๋ ๋๊ฐ ์ฒ๋ฆฌํ๋ ๊ฒ๋ค:
- JavaScript ์คํ (์ฌ๋ฌ๋ถ ์ฝ๋)
- DOM ์ ๋ฐ์ดํธ (๋ ๋๋ง)
- ์ ์ ์ ๋ ฅ ์ฒ๋ฆฌ (ํด๋ฆญ, ํ์ดํ)
- ํ์ด๋จธ๋ ์ ๋๋ฉ์ด์ (requestAnimationFrame, setTimeout)
- ๋คํธ์ํฌ ์ฝ๋ฐฑ (fetch ์๋ต)
500ms ๊ฑธ๋ฆฌ๋ ํจ์๋ฅผ ์คํํ๋ฉด, ๋ค๋ฅธ ๋ชจ๋ ๊ฒ ๊ธฐ๋ค๋ ค์. ๋ธ๋ผ์ฐ์ ๊ฐ ๋ค์ ๊ทธ๋ฆฌ์ง๋ ๋ชปํด์. ํด๋ฆญ๋ ์ฒ๋ฆฌ ๋ชป ํด์. ์ ์ ์ ์ฅ์์ ํ์ด์ง๊ฐ ์ผ์ด๋ฒ๋ฆฐ ๊ฑฐ์์.
// ์ด๊ฑด ๋ฉ์ธ ์ค๋ ๋๋ฅผ ~500ms ๋์ ๋ธ๋กํนํจ function heavyComputation(data) { // 50,000๊ฐ ์์ดํ ์ ์ฒ๋ฆฌํ๋ค ์น๋ฉด return data.map(item => { // ๋ณต์กํ ๋ณํ return expensiveOperation(item); }); } // ์ด๊ฒ ์คํ๋๋ฉด UI ์ผ์ด๋ฒ๋ฆผ const result = heavyComputation(hugeDataset);
"async/await ์ฐ๋ฉด ๋๋ ๊ฑฐ ์๋?"๋ผ๊ณ ์๊ฐํ ์ ์๋๋ฐ, ์ฌ๊ธฐ์ ์ ํตํด์:
// ์ฌ์ ํ ๋ธ๋กํน๋จ! async๋ I/O์๋ง ๋์๋๊ณ CPU ์์ ์ ์์ฉ์์ async function stillBlocking(data) { // ์ด ๊ณ์ฐ์ ์ฌ์ ํ ๋ฉ์ธ ์ค๋ ๋์์ ๋์๊ฐ return data.map(item => expensiveOperation(item)); }
async ํค์๋๋ ์ธ๋ถ ๋ญ๊ฐ๋ฅผ ๊ธฐ๋ค๋ฆด ๋๋ง ๋์์ด ๋ผ์ (๋คํธ์ํฌ, ๋์คํฌ). CPU ๋จน๋ ์์
์๋ ์ง์ง ๋ณ๋ ฌ ์ฒ๋ฆฌ๊ฐ ํ์ํด์. ๊ทธ๊ฒ ๋ฐ๋ก Web Workers๊ณ ์.
Web Workers ๊ธฐ๋ณธ ๊ฐ๋
Web Worker๋ ๋ฉ์ธ ์ค๋ ๋๋ ๋ณ๋ ฌ๋ก ๋์๊ฐ๋ ๋ณ๋์ JavaScript ์ค๋ ๋์์. ์์ฒด ์ด๋ฒคํธ ๋ฃจํ, ์์ฒด ๊ธ๋ก๋ฒ ์ค์ฝํ๊ฐ ์๊ณ , ์ค์ํ ๊ฑด DOM์ ์ ๊ทผํ ์ ์์ด์.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ๋ธ๋ผ์ฐ์ ํ๋ก์ธ์ค โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ ๋ฉ์ธ ์ค๋ ๋ โ ์์ปค ์ค๋ ๋ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โข DOM ์ ๊ทผ ๊ฐ๋ฅ โ โข DOM ์ ๊ทผ ๋ถ๊ฐ โ
โ โข window ๊ฐ์ฒด โ โข self ๊ฐ์ฒด โ
โ โข ์ ์ ์ด๋ฒคํธ โ โข ๋ฌด๊ฑฐ์ด ๊ณ์ฐ โ
โ โข ๋ ๋๋ง โ โข ๋ฐ์ดํฐ ์ฒ๋ฆฌ โ
โ โข React ์ํ โ โข ๋ฐฑ๊ทธ๋ผ์ด๋ ์์
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ postMessage() / onmessage โ
โ (structured cloning์ผ๋ก ํต์ ) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
์ค๋ ๋ ๊ฐ ํต์ ์ postMessage()๋ onmessage๋ก ํด์. ๋ฐ์ดํฐ๋ "structured clone algorithm"์ ํตํด ์ค๋ ๋ ๊ฐ์ ๋ณต์ฌ๋ผ์. ๊ณต์ ๊ฐ ์๋๋ผ ๋ณต์ฌ์์. ๋๋ถ๋ถ์ JavaScript ํ์
์ ์ง์ํ๋ ๋ฅ ์นดํผ๋ผ๊ณ ๋ณด๋ฉด ๋ผ์.
๊ฐ์ฅ ๊ฐ๋จํ Worker ์์:
// worker.js self.onmessage = function(event) { const data = event.data; const result = heavyComputation(data); self.postMessage(result); }; function heavyComputation(data) { // ๋ฌด๊ฑฐ์ด ์์ ์ฌ๊ธฐ์ return data.map(item => item * 2); }
// main.js const worker = new Worker('worker.js'); worker.onmessage = function(event) { console.log('๊ฒฐ๊ณผ:', event.data); }; worker.postMessage([1, 2, 3, 4, 5]);
์ถฉ๋ถํ ๊ฐ๋จํ์ฃ ? ๊ทผ๋ฐ ์ฌ๊ธฐ์ ์๋ฌธ์ด ์๊ฒจ์. React์์ ์ด๋ป๊ฒ ์จ? TypeScript๋? ๋ณต์กํ ๋ก์ง์ ์ด๋ป๊ฒ ๊ณต์ ํด? ์ค์ ์๋ฃจ์ ์ ๋ง๋ค์ด๋ด ์๋ค.
๋ชจ๋ React์์ Web Workers ์ธํ ํ๊ธฐ
Vite, Next.js, Create React App ์ฐ๊ณ ์๋ค๋ฉด, ๊ฐ๊ฐ Worker ์ง์ ๋ฐฉ์์ด ์ข ๋ฌ๋ผ์. ๋ชจ๋ ํ๊ฒฝ์์ ๋จนํ๋ ๋ชจ๋ํ ๋ฐฉ๋ฒ๋ค ์๋ ค๋๋ฆด๊ฒ์.
๋ฐฉ๋ฒ 1: Blob URL๋ก ์ธ๋ผ์ธ Worker
๊ฐ๋จํ ์ผ์ด์ค์์๋ ์ธ๋ผ์ธ ์ฝ๋๋ก Worker๋ฅผ ๋ง๋ค ์ ์์ด์:
// utils/createWorker.ts export function createWorkerFromFunction<T, R>( fn: (data: T) => R ): (data: T) => Promise<R> { const workerCode = ` self.onmessage = function(e) { const fn = ${fn.toString()}; const result = fn(e.data); self.postMessage(result); }; `; const blob = new Blob([workerCode], { type: 'application/javascript' }); const workerUrl = URL.createObjectURL(blob); const worker = new Worker(workerUrl); return (data: T): Promise<R> => { return new Promise((resolve, reject) => { worker.onmessage = (e) => resolve(e.data); worker.onerror = (e) => reject(e); worker.postMessage(data); }); }; }
์ฐ๋ ๋ฒ:
const processInWorker = createWorkerFromFunction((numbers: number[]) => { return numbers.map(n => n * n).reduce((a, b) => a + b, 0); }); // ์ด์ Worker์์ ๋์๊ฐ! const sum = await processInWorker([1, 2, 3, 4, 5]);
์ ํ์ฌํญ: ํจ์๊ฐ ๋ค๋ฅธ ๋ชจ๋ importํ๊ฑฐ๋ ํด๋ก์ ์ธ ์ ์์ด์. ์์ ํ ๋ ๋ฆฝ์ ์ด์ด์ผ ํด์.
๋ฐฉ๋ฒ 2: Vite ๋ค์ดํฐ๋ธ Worker ์ง์
Vite๋ ?worker ์ ๋ฏธ์ฌ๋ก ๊น๋ํ Worker ์ง์์ ์ ๊ณตํด์:
// workers/dataProcessor.worker.ts export interface WorkerInput { data: number[]; operation: 'sum' | 'average' | 'max'; } export interface WorkerOutput { result: number; processingTime: number; } self.onmessage = (event: MessageEvent<WorkerInput>) => { const start = performance.now(); const { data, operation } = event.data; let result: number; switch (operation) { case 'sum': result = data.reduce((a, b) => a + b, 0); break; case 'average': result = data.reduce((a, b) => a + b, 0) / data.length; break; case 'max': result = Math.max(...data); break; } const output: WorkerOutput = { result, processingTime: performance.now() - start, }; self.postMessage(output); };
// hooks/useDataProcessor.ts import { useCallback, useRef, useEffect } from 'react'; import DataProcessorWorker from '../workers/dataProcessor.worker?worker'; import type { WorkerInput, WorkerOutput } from '../workers/dataProcessor.worker'; export function useDataProcessor() { const workerRef = useRef<Worker | null>(null); useEffect(() => { workerRef.current = new DataProcessorWorker(); return () => workerRef.current?.terminate(); }, []); const process = useCallback((input: WorkerInput): Promise<WorkerOutput> => { return new Promise((resolve, reject) => { if (!workerRef.current) { reject(new Error('Worker not initialized')); return; } workerRef.current.onmessage = (e) => resolve(e.data); workerRef.current.onerror = (e) => reject(e); workerRef.current.postMessage(input); }); }, []); return { process }; }
๋ฐฉ๋ฒ 3: Comlink๋ก ๊น๋ํ API ๋ง๋ค๊ธฐ
๊ตฌ๊ธ ํฌ๋กฌ ๋ฉ์ค์์ ๋ง๋ Comlink๋ Worker๋ฅผ ์ผ๋ฐ async ํจ์์ฒ๋ผ ์ธ ์ ์๊ฒ ํด์ค์:
// workers/imageProcessor.worker.ts import * as Comlink from 'comlink'; const api = { async processImage(imageData: ImageData): Promise<ImageData> { // ๋ฌด๊ฑฐ์ด ์ด๋ฏธ์ง ์ฒ๋ฆฌ const pixels = imageData.data; for (let i = 0; i < pixels.length; i += 4) { // ๊ทธ๋ ์ด์ค์ผ์ผ ๋ณํ const avg = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3; pixels[i] = avg; // R pixels[i + 1] = avg; // G pixels[i + 2] = avg; // B } return imageData; }, async analyzeData(data: number[]): Promise<{ mean: number; stdDev: number; median: number; }> { const sorted = [...data].sort((a, b) => a - b); const mean = data.reduce((a, b) => a + b, 0) / data.length; const squaredDiffs = data.map(x => Math.pow(x - mean, 2)); const variance = squaredDiffs.reduce((a, b) => a + b, 0) / data.length; return { mean, stdDev: Math.sqrt(variance), median: sorted[Math.floor(sorted.length / 2)], }; }, }; Comlink.expose(api); export type WorkerApi = typeof api;
// hooks/useImageProcessor.ts import { useEffect, useRef } from 'react'; import * as Comlink from 'comlink'; import type { WorkerApi } from '../workers/imageProcessor.worker'; export function useImageProcessor() { const workerRef = useRef<Comlink.Remote<WorkerApi> | null>(null); useEffect(() => { const worker = new Worker( new URL('../workers/imageProcessor.worker.ts', import.meta.url), { type: 'module' } ); workerRef.current = Comlink.wrap<WorkerApi>(worker); return () => worker.terminate(); }, []); return workerRef.current; } // ์ปดํฌ๋ํธ์์ ์ฐ๋ ๋ฒ function ImageEditor() { const processor = useImageProcessor(); const handleProcess = async () => { if (!processor) return; // ๊ทธ๋ฅ async ํจ์ ํธ์ถํ๋ฏ์ด! const result = await processor.processImage(imageData); setProcessedImage(result); }; }
Comlink๊ฐ postMessage ์ฒ๋ฆฌ๋ฅผ ๋ค ํด์ฃผ๋๊น Worker๊ฐ ํจ์ฌ ์์ฐ์ค๋ฝ๊ฒ ๋๊ปด์ ธ์.
์ค์ ์์ : UI ์ ๋ฉ์ถ๋ CSV ํ์
๋ญ๊ฐ ์ค์ฉ์ ์ธ ๊ฑธ ๋ง๋ค์ด๋ด ์๋ค. CSV ํ์ผ ์ํฌํธํ๋ ์ฑ์ ๋ง๋ ๋ค๊ณ ํด๋ด์. ์ ์ ๊ฐ 50MB์ง๋ฆฌ, 500,000 ํ CSV๋ฅผ ์ ๋ก๋ํด์. Worker ์์ด๋ ํ์ฑํ๋ ๋์ UI๊ฐ 5-10์ด ๋์ ๋ฉ์ถฐ๋ฒ๋ ค์.
// workers/csvParser.worker.ts import * as Comlink from 'comlink'; interface ParseOptions { delimiter?: string; hasHeader?: boolean; } interface ParseResult { headers: string[]; rows: string[][]; rowCount: number; parseTimeMs: number; } interface ProgressCallback { (progress: number): void; } const csvParser = { async parse( csvText: string, options: ParseOptions = {}, onProgress?: ProgressCallback ): Promise<ParseResult> { const start = performance.now(); const { delimiter = ',', hasHeader = true } = options; const lines = csvText.split('\n'); const totalLines = lines.length; const headers: string[] = []; const rows: string[][] = []; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; const values = parseCsvLine(line, delimiter); if (i === 0 && hasHeader) { headers.push(...values); } else { rows.push(values); } // 1000 ํ๋ง๋ค ์งํ ์ํฉ ๋ฆฌํฌํธ if (onProgress && i % 1000 === 0) { onProgress(Math.round((i / totalLines) * 100)); } } return { headers, rows, rowCount: rows.length, parseTimeMs: performance.now() - start, }; }, async filter( rows: string[][], columnIndex: number, predicate: string, onProgress?: ProgressCallback ): Promise<string[][]> { const result: string[][] = []; for (let i = 0; i < rows.length; i++) { const value = rows[i][columnIndex]; if (value?.toLowerCase().includes(predicate.toLowerCase())) { result.push(rows[i]); } if (onProgress && i % 1000 === 0) { onProgress(Math.round((i / rows.length) * 100)); } } return result; }, async aggregate( rows: string[][], groupByColumn: number, aggregateColumn: number ): Promise<Map<string, number>> { const groups = new Map<string, number[]>(); for (const row of rows) { const key = row[groupByColumn]; const value = parseFloat(row[aggregateColumn]); if (!isNaN(value)) { if (!groups.has(key)) { groups.set(key, []); } groups.get(key)!.push(value); } } const result = new Map<string, number>(); for (const [key, values] of groups) { result.set(key, values.reduce((a, b) => a + b, 0)); } return result; }, }; function parseCsvLine(line: string, delimiter: string): string[] { const result: string[] = []; let current = ''; let inQuotes = false; for (let i = 0; i < line.length; i++) { const char = line[i]; if (char === '"') { inQuotes = !inQuotes; } else if (char === delimiter && !inQuotes) { result.push(current.trim()); current = ''; } else { current += char; } } result.push(current.trim()); return result; } Comlink.expose(csvParser); export type CsvParser = typeof csvParser;
// hooks/useCsvParser.ts import { useEffect, useRef, useState, useCallback } from 'react'; import * as Comlink from 'comlink'; import type { CsvParser } from '../workers/csvParser.worker'; export function useCsvParser() { const workerRef = useRef<Comlink.Remote<CsvParser> | null>(null); const [progress, setProgress] = useState(0); const [isProcessing, setIsProcessing] = useState(false); useEffect(() => { const worker = new Worker( new URL('../workers/csvParser.worker.ts', import.meta.url), { type: 'module' } ); workerRef.current = Comlink.wrap<CsvParser>(worker); return () => worker.terminate(); }, []); const parseFile = useCallback(async (file: File) => { if (!workerRef.current) throw new Error('Worker not ready'); setIsProcessing(true); setProgress(0); try { const text = await file.text(); const result = await workerRef.current.parse( text, { hasHeader: true }, Comlink.proxy((p: number) => setProgress(p)) ); return result; } finally { setIsProcessing(false); setProgress(100); } }, []); return { parseFile, progress, isProcessing, parser: workerRef.current, }; }
// components/CsvImporter.tsx import { useState } from 'react'; import { useCsvParser } from '../hooks/useCsvParser'; export function CsvImporter() { const { parseFile, progress, isProcessing } = useCsvParser(); const [result, setResult] = useState<ParseResult | null>(null); const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => { const file = e.target.files?.[0]; if (!file) return; const parsed = await parseFile(file); setResult(parsed); }; return ( <div> <input type="file" accept=".csv" onChange={handleFileChange} disabled={isProcessing} /> {isProcessing && ( <div className="progress-bar"> <div className="progress-fill" style={{ width: `${progress}%` }} /> <span>์ฒ๋ฆฌ ์ค... {progress}%</span> </div> )} {result && ( <div className="result"> <p>{result.rowCount.toLocaleString()} ํ ํ์ฑ ์๋ฃ</p> <p>์์ ์๊ฐ: {result.parseTimeMs.toFixed(2)}ms</p> <table> <thead> <tr> {result.headers.map((h, i) => ( <th key={i}>{h}</th> ))} </tr> </thead> <tbody> {result.rows.slice(0, 100).map((row, i) => ( <tr key={i}> {row.map((cell, j) => ( <td key={j}>{cell}</td> ))} </tr> ))} </tbody> </table> </div> )} </div> ); }
์ด์ CSV ํ์ฑ์ด ๋ฐฑ๊ทธ๋ผ์ด๋์์ ๋์๊ฐ์. UI๋ ์ฉ์ฉํ๊ฒ ์ด์์๊ณ , ์ ์ ๋ ์งํ๋ฅ ๋ณด๋ฉด์ ๊ธฐ๋ค๋ฆด ์ ์์ด์. ๋ฒ๋ฒ ์? ์์ด์.
๋์ฉ๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ: Transferable Objects
์ค๋ ๋ ๊ฐ์ ๋๋์ ๋ฐ์ดํฐ๋ฅผ ๋ณต์ฌํ๋ฉด ๋๋ ค์. ArrayBuffer, MessagePort, OffscreenCanvas๋ transferable objects๋ผ๊ณ ํด์ ๋ณต์ฌ ์์ด ์ด๋์ํฌ ์ ์์ด์:
// ๋ณต์ฌ ๋์ ์์ ๊ถ ์ด์ const buffer = new ArrayBuffer(1024 * 1024 * 100); // 100MB // ๋๋ฆผ: ๋ฒํผ ์ ์ฒด๋ฅผ ๋ณต์ฌ worker.postMessage({ buffer }); // ๋น ๋ฆ: ์์ ๊ถ ์ด์ (๋ฉ์ธ ์ค๋ ๋์์ buffer ๋ชป ์) worker.postMessage({ buffer }, [buffer]);
์ด๋ฏธ์ง ์ฒ๋ฆฌํ ๋ ์ด๊ฑด ์์ฒญ๋ ์ฐจ์ด์์:
// workers/imageProcessor.worker.ts self.onmessage = async (e: MessageEvent) => { const { imageData } = e.data; // ์ด๋ฏธ์ง ๋ฐ์ดํฐ ์ฒ๋ฆฌ const processed = applyFilter(imageData); // ๋ณต์ฌ ๋ง๊ณ ์ด์ const buffer = processed.data.buffer; self.postMessage({ imageData: processed }, [buffer]); };
// ๋ฉ์ธ ์ค๋ ๋ const offscreen = canvas.transferControlToOffscreen(); worker.postMessage({ canvas: offscreen }, [offscreen]);
๋ณ๋ ฌ ์ฒ๋ฆฌ๋ฅผ ์ํ Worker ํ
Worker ํ๋๋ ์ข์ง๋ง, ์ฌ๋ฌ ๊ฐ๊ฐ ๋ณ๋ ฌ๋ก ์ผํ๋ฉด ๋ ์ข์ฃ . ๊ฐ๋จํ Worker ํ ์ฝ๋์์:
// utils/WorkerPool.ts export class WorkerPool<TInput, TOutput> { private workers: Worker[] = []; private queue: Array<{ data: TInput; resolve: (result: TOutput) => void; reject: (error: Error) => void; }> = []; private activeWorkers = new Set<Worker>(); constructor( private createWorker: () => Worker, private poolSize: number = navigator.hardwareConcurrency || 4 ) { for (let i = 0; i < poolSize; i++) { this.workers.push(this.createWorker()); } } async execute(data: TInput): Promise<TOutput> { return new Promise((resolve, reject) => { const availableWorker = this.workers.find( w => !this.activeWorkers.has(w) ); if (availableWorker) { this.runTask(availableWorker, data, resolve, reject); } else { this.queue.push({ data, resolve, reject }); } }); } private runTask( worker: Worker, data: TInput, resolve: (result: TOutput) => void, reject: (error: Error) => void ) { this.activeWorkers.add(worker); worker.onmessage = (e) => { resolve(e.data); this.activeWorkers.delete(worker); this.processQueue(); }; worker.onerror = (e) => { reject(new Error(e.message)); this.activeWorkers.delete(worker); this.processQueue(); }; worker.postMessage(data); } private processQueue() { if (this.queue.length === 0) return; const availableWorker = this.workers.find( w => !this.activeWorkers.has(w) ); if (availableWorker) { const { data, resolve, reject } = this.queue.shift()!; this.runTask(availableWorker, data, resolve, reject); } } async executeAll(items: TInput[]): Promise<TOutput[]> { return Promise.all(items.map(item => this.execute(item))); } terminate() { this.workers.forEach(w => w.terminate()); this.workers = []; } }
์ด๋ฏธ์ง ๋ณ๋ ฌ ์ฒ๋ฆฌ์ ์ฐ๋ฉด:
const pool = new WorkerPool( () => new Worker(new URL('./imageWorker.ts', import.meta.url)), 4 // 4๊ฐ ์์ปค ๋ณ๋ ฌ๋ก ); // 100๊ฐ ์ด๋ฏธ์ง๋ฅผ 4๊ฐ ์์ปคํํ ๋๋ ์ ์ฒ๋ฆฌ const results = await pool.executeAll(images);
์์ฃผ ํ๋ ์ค์๋ค
์ค์ 1: ์์ ์์ ์ Worker ๋จ๋ฐ
Worker์๋ ์ค๋ฒํค๋๊ฐ ์์ด์. Worker ์์ฑ, ๋ฐ์ดํฐ ์ง๋ ฌํ, ๊ฒฐ๊ณผ ์ญ์ง๋ ฌํ์ ์๊ฐ์ด ๋ค์ด์. ์์ ์์ ์์ ์ด ์ค๋ฒํค๋๊ฐ ๊ณ์ฐ ์๊ฐ๋ณด๋ค ์ปค์ง ์ ์์ด์.
๊ฒฝํ์น: ์์ ์ด 16ms ์ด์ (60fps์์ ํ ํ๋ ์) ๊ฑธ๋ฆด ๋๋ง Worker ์ฐ์ธ์.
// โ ์ด๊ฑด ์ํด - ์ค๋ฒํค๋๊ฐ ์ด๋๋ณด๋ค ํผ worker.postMessage([1, 2, 3]); // ์ซ์ 3๊ฐ ๋ํ๊ธฐ // โ ์ด๊ฑด ์ด๋ - ์ถฉ๋ถํ ๋ฌด๊ฑฐ์์ ์ค๋ฒํค๋ ์์ worker.postMessage(bigArray); // 100,000๊ฐ ์์ดํ ์ฒ๋ฆฌ
์ค์ 2: Worker ์ ๋๊ณ ๋ฐฉ์น
Worker๋ ๋ฉ๋ชจ๋ฆฌ ๋จน์ด์. ๊ธฐ์กด Worker ์ ๋๊ณ ์๋ก ๊ณ์ ๋ง๋ค๋ฉด, ๋ฉ๋ชจ๋ฆฌ ๋์ ๋์:
// โ ๋ฉ๋ชจ๋ฆฌ ๋์ function processData(data) { const worker = new Worker('worker.js'); worker.postMessage(data); // Worker ์ ๋! } // โ ๊น๋ํ๊ฒ ์ ๋ฆฌ function processData(data) { const worker = new Worker('worker.js'); worker.onmessage = (e) => { console.log(e.data); worker.terminate(); // ๋๋๋ฉด ์ ๋ฆฌ }; worker.postMessage(data); }
React์์ cleanup์์ ํญ์ terminate:
useEffect(() => { const worker = new Worker('worker.js'); workerRef.current = worker; return () => { worker.terminate(); // โ ์ธ๋ง์ดํธ๋ ๋ ์ ๋ฆฌ }; }, []);
์ค์ 3: ์๋ฌ ์ฒ๋ฆฌ ์ ํจ
Worker ์๋ฌ๋ ์๋์ผ๋ก ๋ฉ์ธ ์ค๋ ๋ ์๋ฌ ํธ๋ค๋ฌํํ ์ ๊ฐ์:
// โ ์๋ฌ ์กฐ์ฉํ ๋ฌด์๋จ worker.postMessage(data); // โ ์๋ฌ ์ก์์ฃผ๊ธฐ worker.onerror = (error) => { console.error('Worker ์๋ฌ:', error.message); // ์๋ฌ ์ฒ๋ฆฌ }; worker.onmessageerror = (error) => { console.error('๋ฉ์์ง ์๋ฌ:', error); };
์ค์ 4: Worker ์ค๋ ๋๋ ๋ง์๋ฒ๋ฆฌ๊ธฐ
์์ ์ Worker๋ก ์ฎ๊ฒผ๋ค๊ณ ๋ธ๋กํน์์ ํด๋ฐฉ๋๋ ๊ฑด ์๋์์. Worker๊ฐ 5์ด ๋์ ๋๊ธฐ ์์ ๋๋ฆฌ๋ฉด, ๊ทธ ์ฌ์ด ๋ค๋ฅธ ๋ฉ์์ง ์ฒ๋ฆฌ ๋ชป ํด์.
// โ ๊ธด ๋๊ธฐ ์์ ์ด ๋ชจ๋ ๋ฉ์์ง ๋ง์ self.onmessage = (e) => { const result = process5MillionItems(e.data); // 5์ด ๋์ ๋ธ๋กํน self.postMessage(result); }; // โ ์ฒญํฌ๋ก ๋๋ ์ ์ค๊ฐ์ค๊ฐ ์จ์ด ํ ์ฃผ๊ธฐ self.onmessage = async (e) => { const items = e.data; const results = []; const chunkSize = 10000; for (let i = 0; i < items.length; i += chunkSize) { const chunk = items.slice(i, i + chunkSize); results.push(...processChunk(chunk)); // ๋ค๋ฅธ ๋ฉ์์ง ์ฒ๋ฆฌํ ์ ์๊ฒ ์ ๊น ์๋ณด await new Promise(resolve => setTimeout(resolve, 0)); // ์งํ๋ฅ ๋ฆฌํฌํธ self.postMessage({ type: 'progress', value: i / items.length }); } self.postMessage({ type: 'complete', results }); };
Web Workers ์ฐ๋ฉด ์ ๋๋ ๊ฒฝ์ฐ
Worker๊ฐ ๋ง๋ฅ์ ์๋์์. ์ด๋ด ๋ ํผํ์ธ์:
-
I/O ์์ ์ด์ง CPU ์์ ์ด ์๋ ๋ - ๋ฐ์ดํฐ fetching, DB ์ฟผ๋ฆฌ ๊ฐ์ ๊ฑฐ. ๊ทธ๋ฅ async/await ์ฐ๋ฉด ๋ผ์.
-
DOM ๊ฑด๋๋ ค์ผ ํ ๋ - Worker๋ DOM ๋ชป ๋ง์ ธ์. ์ฒ๋ฆฌ ์ค๊ฐ์ UI ์ ๋ฐ์ดํธํด์ผ ํ๋ฉด, ๋ฉ์ธ ์ค๋ ๋ํํ ๋ฉ์์ง ๋ณด๋ด์ผ ํด์.
-
๋ฐ์ดํฐ ์ ์ก ๋น์ฉ์ด ๊ณ์ฐ ์ ์ฝ๋ณด๋ค ํด ๋ - 100ms ๊ณ์ฐ ์๋ผ๋ ค๊ณ 1GB ๋ฐ์ดํฐ ์ง๋ ฌํํ๋ ๊ฑด ๋ณธ์ ๋ ๋ชป ์ฐพ์์.
-
์ด๋ฏธ ๋น ๋ฅธ ์์ ์ผ ๋ - 16ms ์์ ๋๋๋ฉด ์ค๋ฒํค๋๊ฐ ๋ ์ปค์.
-
์ค๋๋ ๋ธ๋ผ์ฐ์ ์ง์ํด์ผ ํ๋๋ฐ ํด๋ฐฑ๋ ์์ ๋ - ๋ธ๋ผ์ฐ์ ์ง์ ๋ฒ์ ์ฒดํฌํ์ธ์.
์ฑ๋ฅ ๋น๊ต: Before vs After
์ค์ ๋ก 100๋ง ๊ฐ ์ซ์ ์ ๋ ฌํด๋ณด๋ฉด:
// Worker ์์ด function sortData() { const start = performance.now(); const sorted = data.sort((a, b) => a - b); console.log(`๋ฉ์ธ ์ค๋ ๋: ${performance.now() - start}ms`); console.log(`์ด ์๊ฐ ๋์ UI ๋ฉ์ถค`); } // Worker๋ก async function sortDataWorker() { const start = performance.now(); const sorted = await workerSort(data); console.log(`Worker: ${performance.now() - start}ms`); console.log(`UI๋ ์ฉ์ฉ`); }
์ผ๋ฐ์ ์ธ ๋จธ์ ์์ ๊ฒฐ๊ณผ:
- Worker ์์ด: 1,200ms (UI ๋ฉ์ถค)
- Worker๋ก: 1,250ms (UI ์ฉ์ฉ)
์ง๋ ฌํ ์ค๋ฒํค๋ ๋๋ฌธ์ ์ด ์๊ฐ์ Worker๊ฐ ์ด์ง ๋ ๊ธธ์ด์. ๊ทผ๋ฐ UI๊ฐ ์ ๋ ์ ๋ฉ์ถ๋๊น ์ฒด๊ฐ ์ฑ๋ฅ์ ๋น๊ต๋ ์ ๋ผ์.
์ ๋ฆฌํ๋ฉด
Web Workers๋ ๋ธ๋ผ์ฐ์ API ์ค์์ ๊ฐ์ฅ ์ ํ๊ฐ๋ ๊ฒ ์ค ํ๋์์. ์ ๋๋ก ์ฐ๋ฉด ๋ป๋ปํ ์ฑ์ ์ฐฐ๋ก๊ฐ์ด ๋ฐ์ํ๋ ์ฑ์ผ๋ก ๋ฐ๊ฟ์ค์.
ํต์ฌ ํฌ์ธํธ:
-
๋ฉ์ธ ์ค๋ ๋๋ ์์คํด์ - ๋ ๋๋ง์ด๋ ์ ์ ๋ฐ์์ฉ์ผ๋ก ์๊ปด๋์ธ์.
-
Worker๋ CPU ๋จน๋ ์์ ์ฉ - ๋ฐ์ดํฐ ์ฒ๋ฆฌ, ํ์ฑ, ๊ณ์ฐ, ์ด๋ฏธ์ง ์กฐ์.
-
Comlink ์ฐ๋ฉด API๊ฐ ๊น๋ํด์ - Worker๊ฐ ๊ทธ๋ฅ async ํจ์์ฒ๋ผ ๋๊ปด์ ธ์.
-
๋ณต์ฌ ๋ง๊ณ ์ ์ก - ํฐ ArrayBuffer๋ transferable objects๋ก.
-
Worker ํ๋ก ๋ณ๋ ฌ ์ฒ๋ฆฌ - ์ฌ๋ฌ ์์ปค๊ฐ ๋์์ ์ผํ๋ฉด ๋ ๋น ๋ฆ.
-
์ต์ ํ ์ ์ ์ธก์ ๋ถํฐ - Worker๋ ์ค๋ฒํค๋ ์์ด์. ์ด๋์ด ์ํด๋ณด๋ค ํด ๋๋ง ์ฐ์ธ์.
-
์ ๋ฆฌ ์์ง ๋ง์ธ์ - ๋๋๋ฉด terminate ๊ผญ.
๋ค์์ React ์ฑ์ด ๋ฌด๊ฑฐ์ด ์์ ๋๋ฌธ์ ๋ฒ๋ฒ ๊ฑฐ๋ฆฌ๋ฉด, ๋ญ ํด์ผ ํ๋์ง ์ด์ ์๊ฒ ์ฃ ? ๊ทธ ์์ Worker๋ก ๋นผ๊ณ , ๋ฉ์ธ ์ค๋ ๋ ๋น์๋๊ณ , ์ ์ ํํ ๋ถ๋๋ฌ์ด ๊ฒฝํ ์ ๋ฌผํ์ธ์.