TIL

🧩Webpack + SharedWorker + TypeScript 환경에서 발생한 트러블슈팅 기록

inz1234 2025. 3. 30. 03:10

React 프로젝트에서 여러 탭 간 WebSocket 연결을 공유하기 위해 SharedWorker를 사용하려고 했고,
백엔드가 Spring Boot 기반이라 STOMP 프로토콜을 클라이언트에서도 사용해야 했다.
하지만 Webpack + TypeScript 환경에서 SharedWorker를 구성하는 과정에서 여러 문제가 발생했다.


🔍 초기 설계 방향과 발생한 오해들

1. 원래 SharedWorker는 이렇게 쓰는 줄 알았다. - 정적 경로 기반 접근에 대한 이해 부족

const worker = new SharedWorker("/worker/sharedWorker.js");
  • 이 방식은 브라우저가 서버의 정적 경로(/worker/sharedWorker.js)로 워커 파일을 직접 요청함
  • 여기서 "서버"는 Webpack dev server 또는 나중에 배포될 정적 파일 서버

하지만 이 방식은 다음과 같은 문제를 가진다:

  • Webpack이 해당 JS 파일을 어디에 출력할지 알지 못함
  • 직접 public/ 폴더에 넣거나, 별도 설정을 통해 파일을 복사해야 함
  • 번들링 후 경로가 바뀔 수 있어 404가 나기 쉽고,
  • 캐싱/해시 전략도 수동으로 관리해야 하므로 Webpack 기반 프로젝트에선 비효율적이다.

❗ 왜 그런 일이 생기냐면?

Webpack은 일반적으로 src/ 내부의 모든 코드를 분석해서 /dist로 번들링한다.
이 때, Webpack은 코드 안의 import, require, new URL(...) 같은 표현을 찾아서 “아! 이 파일도 필요하구나” 하고
자동으로
번들 대상에 포함시킨다.

하지만 아래처럼 정적인 문자열 경로로 접근하면:

new SharedWorker("/worker/sharedWorker.js");
  • 이건 Webpack 입장에선 그냥 문자열일 뿐이다.
  • “sharedWorker.js 파일도 번들에 넣어야겠다!”고 판단하지 못함
  • 그래서 sharedWorker.js는 Webpack의 번들 대상이 아니게 되고 → /dist 폴더에 안 생김

🔥 그래서 어떤 문제가 생기냐면, 런타임에 브라우저는 아래 경로를 직접 요청하려고 한다:

GET https://localhost:3300/worker/sharedWorker.js

그런데 Webpack이 해당 파일을 /dist에 안 넣었기 때문에 → 404 Not Found 발생 ❌
 즉, 빌드는 문제 없는데 실행 시 에러가 터지는 구조다. 


2. 그래서 등장한 게 worker-loader

Webpack에서는 이런 문제를 해결하기 위해 worker-loader라는 로더를 사용했다.

import SharedWorker from "worker-loader!@/worker/sharedWorker.js";
const worker = new SharedWorker();

이렇게 하면:

  • Webpack이 sharedWorker.js 파일을 모듈처럼 해석해서 자동으로 번들링해주고,
  • sharedWorker.[hash].js 같은 파일로 출력해줄 것이라 기대
  • 개발자는 경로 걱정 없이 import만으로 워커를 사용할 수 있으리라..!

하지만 이후 여러 문제에 부딪히며 이 접근이 현재 Webpack5와 잘 맞지 않음을 알게 되었다.


⚠️ 문제 상황 요약

  1. SharedWorker를 worker-loader로 불러오려 했는데 worker.port가 undefined
  2. Webpack 빌드 실패 (ValidationError / child compilation error)
  3. import.meta.url 관련 TypeScript 에러 발생 
  4. STOMP 라이브러리 사용 시 worker에서 import 에러
  5. worker-loader로 import 시 TypeScript 타입 에러 발생

1️⃣ worker-loader로 SharedWorker 불러왔지만 worker.port가 undefined

import SharedWorker from "worker-loader!@/worker/sharedWorker.js";
const worker = new SharedWorker();
worker.port.start(); // ❌ TypeError: Cannot read properties of undefined (reading 'start')
  • worker-loader가 번들한 파일이 아닌, 전역 SharedWorker 생성자를 그대로 불러오는 문제
  • 즉, Webpack이 번들링한 sharedWorker 인스턴스가 아니라 잘못된 값이 import됨
  • worker-loader가 잘 작동하지 않거나, 타입/모듈 해석 문제로 제대로 연결되지 않음
  • worker-loader가 SharedWorker를 default export로 내보냈는데 나는 그걸 그냥 바로 쓰고 있어서 undefined를 출력하나?: import된 객체의 default 속성이 의심되어 다음과 같이 우회 시도
const WorkerConstructor = (SharedWorker as any).default ?? SharedWorker;
const worker = new WorkerConstructor();
worker.port.start();

하지만, 결과는 똑같이 port가 undefined, 결국 타입에러가 아닌 
worker-loader가 SharedWorker를 제대로 번들링하지 못하고 있다고 판단했다.


 

2️⃣ worker-loader에 type: "sharedWorker" 옵션 주자, Webpack 빌드 실패

“아… 그럼 worker-loader가 SharedWorker임을 모르니까 제대로 처리 못하는 건가”
// webpack.config.js 
{
  test: /sharedWorker\.js$/,
  use: [
    {
      loader: "worker-loader",
      options: {
        type: "sharedWorker", // 🧠 SharedWorker 전용으로 번들링하라고 명시!
        filename: "sharedWorker.[contenthash].js",
        inline: "no"
      }
    }
  ]
}

🔥에러

ValidationError: Worker Loader Invalid Options
options['type'] is an invalid additional property

원인:

  • worker-loader@3.x: type 옵션 제거됨
  • worker-loader@2.x: type 옵션 없음 (사용하면 에러)

   worker-loader 자체를 버리고 Webpack5 공식 방식 사용 

Webpack5 공식문서


3️⃣ import.meta.url 사용 시 TypeScript 에러 (최종 해결)

Webpack5에서 공식적으로 제공하는 방식으로 다음과 같이 코드를 변경했는데, 이번에는 또 다음과 같은 에러가 났다.

new SharedWorker(new URL("../worker/sharedWorker.js", import.meta.url))
// ❌ 메타 속성은 --module 옵션이 es2020 , es2022 , esnext ... 인 경우에만 허용됩니다.

최종 해결:

// tsconfig.json
{
  "compilerOptions": {
    "module": "es2020",
    "target": "es2020",
    "lib": ["ESNext", "DOM", "WebWorker"]
  }
}

4️⃣ STOMP 라이브러리 사용 시 worker에서 import 에러

sharedWorker.js 파일에서 stomp.js의 client를 import 하니, 런타임 에러가 발생했다.

원인

  • SharedWorker는 일반 DOM 환경과는 다르게 다른 실행 컨텍스트( sharedWorkerGlobalScope )에서 동작 = 브라우저의 Worker Thread에서 실행됨
  • window나 DOM 접근이 불가능한 백그라운드 환경:
    window, document, alert, localStorage 등 없음 → 대신 self, onconnect, postMessage 등을 사용
  • Webpack 또한 SharedWorker를 메인 번들과는 다른 별도의 entry 파일로 인식 

그래서 SharedWorker 안에서 기존 DOM 코드나 모듈을 import하면, Webpack은 빌드는 해도 런타임에서 ReferenceError 터짐

해결

// sharedWorker.js
importScripts("https://cdn.jsdelivr.net/npm/@stomp/stompjs@7.0.0/bundles/stomp.umd.min.js");
  • CDN으로 직접 import
    • 다만 이 경우, 타입스크립트에서 타입 추론 불가(STOMP의 타입 정의는 npm 패키지로 따로 있음) 

📌 회고

  • 정적 경로 접근은 Webpack의 번들링 시스템과 어긋날 수 있고, 브라우저 런타임 시점에서 404를 유발할 수 있다는 걸 처음 알게 되었다.
  • worker-loader는 버전에 따라 기능이 다르고, 공식 문서도 오래된 정보가 많아 삽질이 길어졌다😵‍💫
  • Webpack 5에서는 더 이상 worker-loader에 의존하지 않고 new URL(..., import.meta.url) 방식으로 접근하는 것이 훨씬 안정적이고 깔끔하다.
  • import.meta.url 관련 에러는 tsconfig 설정이 핵심이라는 걸 깨달았다.
  • sharedWorker.ts 파일로 만들었을 때, 타입스크립트에서 webpack 특수 import 구문(worker-loader!)을 사용할 땐 따로 타입을 정의해야 한다는 것도 중요한 배움이었다(하단 코드)
  • 무엇보다, worker-loader가 뭘 해주는지 제대로 이해하지 않고 쓰기 시작한 게 많은 문제의 출발점이었다.
    로더가 어떤 역할을 하는지, 그 한계는 무엇인지 파악하고 써야겠다고 느꼈다.
  • 에러가 생겼을 때 무작정 포기하기보다, 에러 로그의 흐름을 좁혀가면서 원인을 분석하는 힘이 중요하다는 걸 다시 느꼈다.
// src/types/worker-loader.d.ts

declare module "worker-loader!*" {
  class WebpackSharedWorker extends SharedWorker {
    constructor();
  }
  export default WebpackSharedWorker;
}