일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- virtaullist
- context.api
- 자바스크립트
- Web
- 회고
- 오블완
- 티스토리챌린지
- 리액트
- 웹워커
- provider 패턴
- 이것저것
- frontend
- JavaScript
- sharedworker
- MFA
- 리팩토링
- 에세이
- 프론트엔드
- vite
- CustomHook
- CRA
- radixui
- 클린코드
- TypeScript
- MicroFrontEnd
- Webworker
- 아키텍처
- react
- 합성 컴포넌트
- server component
- Today
- Total
Lighthouse of FE biginner
[트러블슈팅] WebWorker 빌드 시 worker 파일이 포함이 안되는 문제 본문
Overview
이전 글에서 WebWorker, SharedWorker에 대해서 살펴보고 WebSocket을 얹어서 서버와 통신하는 예제를 구현해봤습니다.
해당 코드로 추가적인 리팩토링 작업을 하던 중 로컬의 개발 서버에서는 정상 동작 하지만 빌드 시 worker파일을 빌드에 포함하지 못하는 문제를 확인했습니다. 하여 worker 파일을 빌드하지 않았기 때문에 빌드 결과물을 실행했을 때 worker 스크립트를 찾지 못해서 정상적으로 동작하지 못하는 현상이 발생했습니다.
빌드 디렉토리인 dist를 확인해도 worker 스크립트는 찾아볼 수 없습니다.
해결 과정
문제 도출
먼저 번들링 과정에서 왜 제외가 된 것일까 곰곰히 생각을 해봤습니다. Vite는 번들러로 rollUp을 사용합니다. rollUp 번들러는 번들링 중 용량을 최대한 압축하기 때문에 사용하지 않는 파일을 번들링에 포함하지 않습니다. (트리 쉐이킹)
기존에 WebWorker을 사용하던 방식을 살펴봅니다.
function useWebWorker<T>({ url, initialData }: Props<T>): Result<T> {
const worker = useRef<Worker | null>(null);
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState<T | null>(initialData ?? null);
const sendMessage = useCallback((message?: string) => {
if (worker.current) {
setLoading(true);
worker.current.postMessage(message);
}
}, []);
useEffect(() => {
worker.current = new Worker(new URL(url, import.meta.url));
...생략
소스 경로 상 WebWorker의 위치(string)를 url 이라는 인자로 받아서 워커 생성자에 들어가는 URL로 넣어주고 있습니다.
이 방식은 WebWorker 객체를 런타임에 생성하는 방식으로 개발 서버에서는 런타임에 해당 url의 WebWorker 스크립트가 존재하여 참조가 가능하지만, 빌드 시점에는 해당 스크립트를 참조하고 있지 않기 때문에 빌드에서 제외됩니다.
그렇기 때문에 다른 방식으로 WebWorker을 사용하는 것으로 수정해봅시다.
해결 방안 도출
빌드에 스크립트를 사용하기 위해서 스크립트를 직접 import 하기로 결정했습니다. Vite에서는 WebWorker을 import 하면 해당 스크립트의 WebWorker의 생성자를 리턴해줍니다. 일반 WebWorker는 경로 뒤에 ?worker를, SharedWorker은 경로 뒤에 ?sharedworker을 붙여주면 생성자를 import 할 수 있습니다.
import Worker from "./socket-worker?worker";
import SharedWorker from "./socket-worker?sharedworker";
해당 import 참조를 확인해보면 다음과 같습니다.
// Vite의 client.d.ts 파일입니다.
// web worker
declare module '*?worker' {
const workerConstructor: {
new (options?: { name?: string }): Worker
}
export default workerConstructor
}
declare module '*?sharedworker' {
const sharedWorkerConstructor: {
new (options?: { name?: string }): SharedWorker
}
export default sharedWorkerConstructor
}
Web Workers
이전에 작성했던 useWebWorker 훅에 대해서 다시 한번 생각했습니다. 해당 훅은 여러 WebWorker 스크립트를 공통으로 사용할 수 있도록 디자인 된 커스텀 훅 입니다.
WebWorker 스크립트는 실제 비즈니스 로직을 담고있는 스크립트 파일 입니다. 각각의 WebWorker마다 사용하는 이유, 비즈니스 로직이 다 다를 뿐더러 과연 WebWorker를 사용하기 위해 추상화 한 공용 커스텀 훅이 어울리는 방식일까 라는 고민을 했습니다.
결국 WebWorker와 커스텀 훅을 1:1 방식으로 사용하는 것으로 결정했습니다.
커스텀 훅 수정
커스텀 훅을 수정합니다. 기존에는 WebWorker를 공통으로 사용하기 위해 WebWorker에서 응답받는 메세지의 타입을 결정하기 위해 제네릭 타입을 사용했습니다. 하지만 수정하는 방식에서는 1:1로 WebWorker 스크립트에 대응하기 때문에 코드 작성 시점에 이미 타입을 알고 있습니다. 그렇기 때문에 제네릭 타입을 걷어내고 실제 사용하는 타입인 SocketData를 넣어주도록 합니다.
그 다음 WebWorker 스크립트를 사용하기 위해 WebWorker 파일을 import 합니다. 리턴받은 Worker 생성자를 통해 Worker 객체를 생성하고 ref에 바인딩 합니다.
그 외의 코드는 동일합니다.
import { useCallback, useEffect, useRef, useState } from "react";
import { SocketData } from "../../types/socket-data";
import SharedWorker from "../../workers/shared-worker?sharedworker";
type Props = {
initialData?: SocketData;
};
type Result = {
message: SocketData | null;
loading: boolean;
sendMessage: (message?: string) => void;
};
function useSharedWorker({ initialData }: Props): Result {
const worker = useRef<SharedWorker | null>(null);
const [loading, setLoading] = useState(false);
const [message, setMessage] = useState<SocketData | null>(
initialData ?? null
);
const sendMessage = useCallback((message?: string) => {
if (worker.current) {
setLoading(true);
worker.current.port.postMessage(message);
}
}, []);
useEffect(() => {
worker.current = new SharedWorker();
worker.current.port.onmessage = (event: MessageEvent<string>) => {
let messageData = null;
const { data } = event;
try {
messageData = JSON.parse(data);
} catch {
messageData = data;
}
setLoading(false);
setMessage(messageData);
};
if (worker.current) {
worker.current.port.start();
}
return () => {
if (worker.current) {
worker.current.port.close();
}
};
}, []);
return {
message,
loading,
sendMessage,
};
}
export default useSharedWorker;
검증
빌드를 실행해 트러블슈팅한 내용이 잘 반영됐는지 체크합니다.
빌드 결과물에 worker 스크립트가 잘 반영된 것을 확인할 수 있습니다! 이제 빌드 결과물을 실행해보도록 합니다.
Vite의 빌드 결과물이 실행되는 포트는 4173번 입니다. 위 결과물을 확인해보면 worker 파일을 잘 불러와 실제 서버와 잘 통신하는 것을 확인할 수 있습니다!
개발 서버에서 실행했던 내용과 동일하게 잘 작동하는 것을 확인할 수 있습니다!
마치며
이전에 예제를 작성할 때 빌드를 고려하는 것을 깜빡했는데, 금일 빌드 과정에서 문제점이 있다는 사실을 확인하고 식겁했습니다.
곰곰히 원인을 생각해보고 문제를 해결하는데 2~3시간을 보냈는데 고민하는 시간이 정말 행복한 시간이였습니다.
예제를 작성하는 시점에 빌드 타임을 고려해야 하는 것을 확인했으니, 실제로 프로젝트에서 적용을 할 때 문제를 덜어볼 수 있을것 같습니다.
현재 개발하는 솔루션에서 엑셀 파일을 프론트엔드 단에서 만들어 다운로드 해주고 있는데 해당 파싱 과정에서 WebWorker를 이용해 UX와 다운로드 단계 까지의 속도를 개선할 수 있지 않을까 라는 생각도 하고 있습니다.
다음번에 기회가 된다면 실제로 프로젝트에 WebWorker를 적용해보고 적용하며 겪은 과정을 남겨보도록 하겠습니다.
트러블 슈팅한 코드는 아래 저장소에서 확인할 수 있습니다. 이전의 저장소와는 다르게 모노레포를 적용한 저장소 입니다.
저장소 바로가기
읽어봐주셔서 감사합니다!
'[WEB] 프론트엔드' 카테고리의 다른 글
[MFA] Micro App의 관심사 공유 (0) | 2025.02.13 |
---|---|
[React] 합성(Composition) 컴포넌트 (2) | 2025.01.01 |
SharedWorker에 WebSocket 얹어보기 (6) | 2024.10.17 |
Web Worker 알아보기 (3) | 2024.10.13 |
MFA (Micro Frontend Architecture) (1) | 2024.10.03 |