Angular에서 Transferable Objects로 Worker 성능 최적화하기

Adam Kim·2025년 11월 14일
0

angular

목록 보기
70/88

소개

웹 워커(Worker)는 복잡한 계산이나 무거운 작업을 별도의 스레드에서 실행하여 메인 스레드의 부하를 줄이고 애플리케이션의 반응성을 높이는 강력한 도구입니다.
하지만 메인 스레드와 워커 간에 대용량 데이터를 주고받을 때, 데이터를 복사(copy)하는 과정에서 성능 저하가 발생할 수 있습니다. 이때 Transferable Objects를 사용하면 이러한 문제를 효과적으로 해결할 수 있습니다.

Transferable Objects란?

Transferable Objects는 메인 스레드와 워커 사이에서 데이터를 '복사'하는 대신, 데이터의 '소유권'을 이전(transfer)하는 방식입니다. 소유권이 이전된 데이터는 원래 스레드에서 더 이상 접근할 수 없게 되며, 이로 인해 데이터 복사에 드는 오버헤드를 거의 없앨 수 있습니다.
주로 사용되는 Transferable Objects는 다음과 같습니다.

  • ArrayBuffer: 바이너리 데이터를 다루는 객체로, 메모리 소유권을 이전하는 데 가장 기본적으로 사용됩니다.
  • MessagePort: 워커 간 직접적인 통신 채널을 생성하며, 이 채널 자체를 이전할 수 있습니다.
  • ImageBitmap: 이미지 데이터를 처리할 때 효율적으로 전달하는 데 사용됩니다.

Transferable Objects 활용 예시

이제 Angular 애플리케이션에서 ArrayBuffer를 Transferable Object로 활용하여 워커와 통신하는 현대적인 예시 코드를 살펴보겠습니다.

1. Worker 스크립트 (app.worker.ts)

백그라운드 스레드에서 실행될 워커 파일입니다. 메인 스레드로부터 ArrayBuffer 형태의 데이터를 받아 작업을 수행하고, 결과 또한 ArrayBuffer로 변환하여 소유권을 이전하는 방식으로 다시 전송합니다.

/// <reference lib="webworker" />

// 메인 스레드로부터 메시지를 수신합니다.
addEventListener('message', ({ data }) => {
  // 전달받은 ArrayBuffer를 JSON 객체로 디코딩합니다.
  const obj = decodeBuffer(data);

  // 복잡한 계산을 수행합니다 (예시: 제곱의 합).
  let sum = 0;
  for (let i = obj.min; i <= obj.max; i++) {
    sum += Math.pow(i, 2);
  }

  // 계산 결과를 다시 ArrayBuffer로 인코딩합니다.
  const resultBuffer = convertToBuffer(sum);

  // postMessage의 두 번째 인자로 ArrayBuffer를 전달하여 소유권을 이전합니다.
  postMessage(resultBuffer, [resultBuffer]);
});

// ArrayBuffer를 JSON 객체로 변환하는 헬퍼 함수
function decodeBuffer(buffer: ArrayBuffer): any {
  const jsonString = new TextDecoder().decode(buffer);
  return JSON.parse(jsonString);
}

// JavaScript 값을 ArrayBuffer로 변환하는 헬퍼 함수
function convertToBuffer(data: any): ArrayBuffer {
  const jsonString = JSON.stringify(data);
  return new TextEncoder().encode(jsonString).buffer;
}

2. Worker 관리 서비스 (transferable-worker.service.ts)

워커를 생성하고 데이터 통신을 관리하는 서비스 클래스입니다. 컴포넌트에서는 이 서비스의 인스턴스를 생성하여 워커를 제어합니다.

// transferable-worker.service.ts
import { signal, WritableSignal } from '@angular/core';

export class TransferableWorkerService {
  private worker: Worker;

  // Signal을 사용하여 워커의 상태를 관리합니다.
  public readonly result: WritableSignal<any> = signal(undefined);
  public readonly isLoading: WritableSignal<boolean> = signal(false);
  public readonly error: WritableSignal<any> = signal(undefined);

  constructor() {
    if (typeof Worker !== 'undefined') {
      this.worker = new Worker(new URL('./app.worker', import.meta.url));

      this.worker.onmessage = ({ data }: MessageEvent<ArrayBuffer>) => {
        const resultString = new TextDecoder().decode(data);
        this.result.set(JSON.parse(resultString));
        this.isLoading.set(false);
      };

      this.worker.onerror = (err) => {
        this.error.set(err);
        this.isLoading.set(false);
        console.error('Worker error:', err);
      };
    } else {
      console.error('이 환경은 웹 워커를 지원하지 않습니다.');
    }
  }

  public calculate(min: number = 1, max: number = 10000000): void {
    // 작업 시작 전 상태 초기화
    this.isLoading.set(true);
    this.result.set(undefined);
    this.error.set(undefined);

    const data = { min, max };
    const dataBuffer = this.convertToBuffer(data);

    // ArrayBuffer의 소유권을 워커로 이전
    this.worker.postMessage(dataBuffer, [dataBuffer]);
  }

  private convertToBuffer(jsonData: any): ArrayBuffer {
    return new TextEncoder().encode(JSON.stringify(jsonData)).buffer;
  }

  public destroy(): void {
    if (this.worker) {
      this.worker.terminate();
    }
  }
}

3. 컴포넌트 (app.component.ts)

컴포넌트에서는 TransferableWorkerService를 사용하여 워커에게 작업을 요청하고, Signal을 통해 상태 변화를 감지하여 UI를 렌더링합니다.

// app.component.ts
import { Component, OnDestroy } from '@angular/core';
import { TransferableWorkerService } from './transferable-worker.service';

@Component({
  selector: 'app-root',
  standalone: true,
  template: `
    <h2>Transferable Objects Worker 예제</h2>
    <button (click)="runCalculation()">대용량 계산 실행</button>

    @if (worker.isLoading()) {
      <p>계산 중...</p>
    }

    @if (worker.result(); as result) {
      <p>결과: {{ result }}</p>
    }

    @if (worker.error(); as error) {
      <p>에러가 발생했습니다: {{ error.message }}</p>
    }
  `,
})
export class AppComponent implements OnDestroy {
  // 컴포넌트 내에서 독립적인 워커 서비스 인스턴스를 생성
  readonly worker = new TransferableWorkerService();

  runCalculation(): void {
    this.worker.calculate();
  }

  ngOnDestroy(): void {
    // 컴포넌트가 파괴될 때 워커 리소스를 반드시 정리합니다.
    this.worker.destroy();
  }
}

결론

Transferable Objects를 활용하면 메인 스레드와 워커 간의 데이터 전송 성능을 극적으로 향상시킬 수 있습니다. 특히 대용량 데이터를 다루거나 병렬 처리가 필요한 웹 애플리케이션에서 이를 적극적으로 활용하여 성능을 최적화할 수 있습니다. 데이터 복사 비용을 최소화하고 메모리를 효율적으로 사용하는 것이 핵심입니다.

profile
Angular2+ Developer

0개의 댓글