반응형
snowman95
코딩수련장
snowman95
전체 방문자
오늘
어제
  • 분류 전체보기 (250)
    • 개발자 글 수집 (11)
    • 앱테크 (3)
    • 옵시디언 (5)
    • 드라마, 영화 (1)
    • 개발자 이야기 (28)
    • 프로젝트 (11)
      • 프로젝트 방법론 (7)
      • 프로젝트 기록 (3)
      • Github (1)
    • 개발 지식 (0)
      • 디자인 패턴 (0)
    • 프론트엔드 개발 (101)
      • 시니어 시리즈 (3)
      • 테크트리 (2)
      • React.js (19)
      • ReactNative (3)
      • Next.js (6)
      • GraphQL (6)
      • 패키지 매니저 (2)
      • 라이브러리 (3)
      • 상태관리 라이브러리 (4)
      • Web 지식 (3)
      • HTML CSS (26)
      • Javascript (16)
      • 도구 (Tool) (3)
      • 성능 최적화 (1)
      • 디자인시스템 (0)
    • Python (53)
      • 모음집 (1)
      • 문법 (12)
      • 라이브러리 (15)
      • 알고리즘 (10)
      • 백준 문제풀이 (9)
      • 코딩테스트 (2)
      • 도구 (Tool) (3)
    • C++ (21)
      • 알고리즘 (7)
      • 삼성SW기출 (6)
      • 삼성 A형 (6)
    • 데이터사이언스 (1)
    • 인프라 (9)
      • 하드웨어 지식 (4)
      • Ansible (2)
      • Database (2)
      • 쉘스크립트 (1)
    • 주식 (0)
    • 취업 준비 (4)
      • 취업 이야기 (0)

블로그 메뉴

  • 홈
  • 태그

공지사항

인기 글

태그

  • 언어
  • claudecode
  • 면접
  • 알고리즘
  • Next.js #graphql #tailwind.css
  • 세차장테스트
  • 클로드코드
  • 25년도채용시장
  • C++
  • AI
  • 개발자이직회고
  • 티스토리챌린지
  • A형
  • 삼성SDS
  • 삼성SW역량테스트
  • 백준
  • 오블완
  • OpenClaw
  • 개발자취업시장
  • 오픈클로

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
snowman95

코딩수련장

[시니어 FE] Web Worker 에 대해 알아보자 (Comlink)
프론트엔드 개발/시니어 시리즈

[시니어 FE] Web Worker 에 대해 알아보자 (Comlink)

2026. 3. 5. 19:48
728x90
반응형

시니어 FE 시리즈

시니어 프론트엔드 개발자가 되기위해 해보지 않았던 경험의 틈을 채우거나 기술적으로 딥다이브 해보고자 준비한 시리즈입니다.

 

그 동안 Web Worker를 막연하게 알고만 있었지 실제로 써볼 기회가 없었습니다.
엄청나게 무거운 정보를 실시간으로 화면에 보여줘야하는 서비스를 만들어본 적이 없었기 때문이죠.

그치만 내가 안해봤다고 모른다고 앞으로도 그런 상황이 주어졌을때 Web Worker 생각을 못한다면 안되겠죠?

 


1. Web Worker의 기본 구조 (Concept)

Web Worker는 브라우저의 메인 스레드와 별개로 배경(Background)에서 돌아가는 별도의 스레드입니다.

  • 메인 스레드: UI 렌더링, 사용자 클릭 이벤트 처리 (DOM 접근 가능)
  • Web Worker: 복잡한 연산, 데이터 가공, 소켓 통신 (DOM 접근 불가)

둘 사이는 postMessage와 onmessage라는 메시지 기반 시스템으로 소통합니다.


2. 실시간 대시보드 설계 구조

대시보드에서 Web Worker를 활용하는 가장 효율적인 파이프라인은 다음과 같습니다.

  1. Worker: WebSocket 연결을 맺고 데이터를 수신합니다.
  2. Worker: 수신된 대량의 원본 데이터(Raw Data)를 UI에 맞게 가공(Filtering, Sorting, 포맷팅)합니다.
  3. Worker -> Main: 가공이 끝난 "최종 결과물"만 메인 스레드로 던집니다.
  4. Main: 전달받은 데이터를 상태(State)에 넣고 화면을 그립니다.

3. 코드 예시 (TypeScript 기반)

① Worker 파일 (price.worker.ts)

Worker 내부에서는 self를 통해 전역 객체에 접근합니다.

// worker 환경에서는 DOM에 접근할 수 없음에 유의하세요.
self.onmessage = (e: MessageEvent) => {
  if (e.data.type === 'CONNECT') {
    const socket = new WebSocket('wss://stock-api.com');

    socket.onmessage = (event) => {
      const rawData = JSON.parse(event.data);

      // [무거운 작업 예시] 수천 개의 데이터 중 5% 이상 변동된 것만 필터링하고 정렬
      const processedData = rawData
        .filter((item: any) => Math.abs(item.change) >= 5)
        .sort((a: any, b: any) => b.price - a.price);

      // 가공된 데이터만 메인 스레드로 전송
      self.postMessage({ type: 'UPDATE_PRICE', data: processedData });
    };
  }
};

② 메인 컴포넌트 (Dashboard.tsx)

React 컴포넌트에서 Worker를 생성하고 메시지를 받습니다.

import { useEffect, useState } from 'react';

const Dashboard = () => {
  const [prices, setPrices] = useState([]);

  useEffect(() => {
    // 1. Worker 인스턴스 생성
    const worker = new Worker(new URL('./price.worker.ts', import.meta.url));

    // 2. Worker로부터 메시지 수신
    worker.onmessage = (e) => {
      if (e.data.type === 'UPDATE_PRICE') {
        setPrices(e.data.data); // 이미 가공된 데이터이므로 바로 상태 업데이트
      }
    };

    // 3. Worker에 시작 명령 전송
    worker.postMessage({ type: 'CONNECT' });

    return () => worker.terminate(); // 컴포넌트 언마운트 시 워커 종료 (메모리 관리)
  }, []);

  return (
    <div>
      {prices.map(p => <PriceItem key={p.id} data={p} />)}
    </div>
  );
};

4. 핵심 포인트 (Tricky Parts)

  • 구조 분해(Structured Clone) 비용: 메인 스레드와 Worker 사이에서 데이터를 주고받을 때 데이터가 복사(Clone)됩니다. 데이터 양이 너무 크면 이 복사 비용 때문에 메인 스레드가 잠깐 멈출 수 있습니다. 이때는 Transferable Objects(ArrayBuffer 등)를 사용해 소유권을 넘겨버리는 방식으로 최적화할 수 있다고 언급하세요.
  • Worker 라이브러리 활용: 실무에서는 Worker 생성을 직접 관리하기보다 Comlink 같은 라이브러리를 사용하여 RPC(Remote Procedure Call) 방식으로 깔끔하게 함수처럼 호출하는 방식을 선호

Transferable Objects와 Comlink를 언급하는 것은
"단순히 기능을 구현하는 수준을 넘어 브라우저의 저수준(Low-level) 메모리 관리와 개발 생산성까지 고려한다"는 강력한 증거가 됩니다.

 

구체적인 예시와 설계

1. Transferable Objects (소유권 이전 방식)

기본적으로 postMessage는 데이터를 복사(Structured Clone)합니다. 100MB 데이터를 보내면 메모리에 100MB가 하나 더 생기고, 이 복사 과정에서 메인 스레드가 블로킹됩니다.
Transferable Objects는 복사하지 않고 데이터의 '소유권(Ownership)'만 워커로 넘깁니다. 넘겨준 쪽(메인 스레드)에서는 더 이상 그 데이터를 쓸 수 없게 되지만, 복사 비용이 0에 가깝습니다.

[코드 예시: ArrayBuffer 활용]

// Main Thread
const buffer = new ArrayBuffer(1024 * 1024 * 100); // 100MB 데이터
const uint8Array = new Uint8Array(buffer);

// 데이터를 채운 뒤 워커로 전송
// 두 번째 인자인 배열 [buffer]가 중요합니다. "이 객체의 소유권을 이전하겠다"는 선언입니다.
worker.postMessage(buffer, [buffer]);

console.log(buffer.byteLength); 
// 출력: 0 (소유권이 이전되었으므로 메인 스레드에서는 데이터가 사라짐)

2. Comlink 활용 (RPC 방식)

postMessage와 onmessage를 직접 쓰면 코드가 매우 파편화되고 if/else 분기 처리가 복잡해집니다. 구글에서 만든 Comlink를 쓰면 워커에 있는 함수를 마치 메인 스레드에 있는 일반 비동기 함수처럼 호출할 수 있습니다.

[Step 1: Worker 정의 (worker.ts)]

import * as Comlink from 'comlink';

const stockService = {
  processedData: [],
  // 복잡한 계산 로직을 메서드로 정의
  async calculateVolatility(data: any[]) {
    // 무거운 연산 수행...
    return data.filter(d => d.volatility > 0.5);
  }
};

// Comlink로 객체를 노출(Expose)시킵니다.
Comlink.expose(stockService);

[Step 2: Main Thread에서 사용 (App.tsx)]

import * as Comlink from 'comlink';

async function init() {
  const worker = new Worker(new URL('./worker.ts', import.meta.url));

  // 워커를 마치 로컬 객체처럼 래핑합니다.
  const service = Comlink.wrap<any>(worker);

  // 워커 내의 함수를 await로 직접 호출합니다 (메시지 이벤트를 직접 짤 필요 없음)
  const result = await service.calculateVolatility(rawData);
  console.log('가공된 데이터:', result);
}

💡 더 발전시키기

"실시간 대시보드처럼 초당 수백 건의 데이터가 들어오는 환경에서는 postMessage의 Structured Clone 비용조차 성능 병목이 될 수 있습니다.
따라서 저는 대량의 바이너리 데이터를 다룰 때는 ArrayBuffer를 Transferable Objects로 넘겨 메인 스레드의 부하를 제로(0)에 가깝게 유지하는 설계를 선호합니다.

또한, 팀 협업 측면에서 postMessage를 직접 다루면 이벤트 핸들러가 비대해져 유지보수가 어렵기 때문에, Comlink 같은 라이브러리를 도입하여 RPC 형태로 추상화함으로써 동료들이 워커 로직을 일반 비동기 함수처럼 쉽고 안전하게 호출할 수 있는 환경을 구축하는것이 좋습니다."


 

Web Worker는 언제 써야 하는가?

"언제 쓰는 것이 적합한가?"라는 질문은 기술의 트레이드오프(비용 대비 이득)를 이해하고 있는지 묻는 것입니다.
Web Worker는 스레드를 생성하고 데이터를 복사(또는 이전)하는 데 비용이 들기 때문에 모든 곳에 쓰면 안 됩니다.


1. Web Worker 도입이 적합한 상황 (Decision Making)

시니어라면 "메인 스레드(UI Thread)의 응답성"을 최우선으로 고려해야 합니다.

  • CPU 집약적인 무거운 연산: * 대량의 JSON 데이터 파싱 및 가공 (수천 개의 주식 데이터 필터링/정렬)
  • 복잡한 수학 계산 (암호화, 이미지/비디오 처리, 캔버스 그래픽 계산)
  • 큰 규모의 텍스트 분석이나 정규식 검사
  • 지속적인 데이터 스트리밍: * WebSocket을 통해 쉼 없이 들어오는 데이터를 실시간으로 처리해야 할 때
  • UI와 독립적인 백그라운드 작업: * 사용자가 화면을 조작하는 동안 영향을 주지 않고 데이터를 미리 페칭(Prefetching)하거나 인덱싱할 때

반대로 부적합한 경우: 단순한 API 호출이나 작은 데이터 처리. 오히려 Worker를 띄우는 오버헤드와 데이터 전송 비용(Serialization)이 더 커서 성능이 저하될 수 있습니다.


2. Web Worker의 종류

크게 3가지로 나뉩니다.

종류 특징 주요 용도
Dedicated Worker 1:1 관계. 생성한 스레드(Tab)에서만 접근 가능. 특정 페이지의 무거운 연산.
Shared Worker 1:N 관계. 동일한 도메인의 여러 탭/윈도우에서 공유 가능. 여러 탭 간의 채팅 연결 공유, 공통 상태 동기화.
Service Worker 프록시 서버 역할. 브라우저와 네트워크 사이에서 동작. 오프라인 캐싱 (PWA), 푸시 알림, 백그라운드 동기화.

3. 방금의 대시보드 예시는 어떤 Worker인가?

우리가 구현한 예시는 Dedicated Worker (전용 워커)입니다.

  • 이유: 주식 대시보드라는 특정 페이지(탭)가 열려 있을 때, 그 페이지를 위한 데이터를 처리하기 위해 생성되었기 때문입니다.
  • 동작 방식: 해당 컴포넌트가 마운트될 때 new Worker()로 생성되고, 언마운트될 때 terminate()`로 종료되는, 단일 컨텍스트에 종속된 형태입니다.

💡 정리

"Web Worker를 써본 적 있나요? 없다면 언제 도입하시겠나요?"라고 물으면 이렇게 답변하세요.

"저는 프레임 드랍 없는 사용자 경험을 중요하게 생각합니다.
일반적으로 브라우저는 16.7ms(60fps 기준) 안에 한 프레임의 작업을 마쳐야 하는데, 대시보드처럼 대량의 실시간 데이터를 가공하는 로직이 메인 스레드에서 16.7ms 이상을 점유할 것으로 판단될 때 Web Worker 도입을 검토합니다.

특히 Dedicated Worker를 사용하여 데이터 수신과 가공을 격리하고, 메인 스레드는 오직 렌더링에만 집중하게 함으로써 복잡한 계산 중에도 사용자 입력이 씹히지 않는 안정적인 아키텍처를 설계하겠습니다."

반응형
저작자표시 비영리 동일조건 (새창열림)

'프론트엔드 개발 > 시니어 시리즈' 카테고리의 다른 글

[시니어 FE] CSRF GET 공격 시나리오와 방어법  (0) 2026.03.07
[시니어 FE] 동시성(Concurrency) 에 대해 알아보자  (0) 2026.03.05
    '프론트엔드 개발/시니어 시리즈' 카테고리의 다른 글
    • [시니어 FE] CSRF GET 공격 시나리오와 방어법
    • [시니어 FE] 동시성(Concurrency) 에 대해 알아보자
    snowman95
    snowman95
    (17~19) Unity/Unreal Engine 게임 프로그래머 (20~21) System Administrator ___________ (22~) React 웹 프론트앤드 개발자 __________ 깃헙 : https://github.com/snowman95

    티스토리툴바