-
Nextjs(typescript) 에서 Web worker 사용하기Nextjs 2024. 2. 1. 22:06
Next js Web worker api
웹툰 번역 툴을 개발하던 중에 psd, psb 파일을 읽어 들이는 시간이 굉장히 오래 걸려서 브라우저가 멈추는 경우가 너무 많았다.
기본적으로 psd, psb 파일은 100MB가 넘는 경우가 많고, 500MB 정도의 크기를 가진 파일도 많았다. 파일을 읽는 동안 메인스레드는 차단되기 때문에, 브라우저가 멈추고 렉이 걸린 것처럼 느껴지는 현상이 있었다. 이 파일 읽기 시간을 어떻게 줄일까 고민을 많이 했고, Web worker를 찾았다.
Web worker API ( https://developer.mozilla.org/ko/docs/Web/API/Web_Workers_API )
Web worker API는 싱글 쓰레드로 동작하는 자바스크립트의 한계를 좀 넓혀주는데, 웹 브라우저에서 멀티 쓰레드를 사용할 수 있게 해준다. 자바스크립트가 멀티 쓰레드로 동작하는 언어였다면 동시성 문제 등 복잡한 문제를 해결해야 하는데 그러지 않고 이벤트 루프, 타임아웃 등으로 비동기 처리가 되게끔 보여지는 싱글 쓰레드로 동작하는 언어가 되었다.
그래서 Next.js 14.0.4에서 Web worker를 사용해서 이미지 처리를 하는 방식으로 구현을 해보았다.
기본 방식은 메인쓰레드에 worker에 postMessage(요청)를 보내고, worker에서 파일을 읽고 가공해서 다시 메인쓰레드로 postMessage를 보낸다. callback으로 받은 결과값을 메인쓰레드에서 적절하게 사용한다.
여러가지 난관에 부딪혔고, 그 과정도 남기려고 한다.
1. Woker.ts 생성하기
// src/app/worker.ts self.addEventListener('message', async ({ data }) => { const { type, timestamp, value } = data // type 별로 분기 }
2. Worker message type 정의하기
/** * @param type * @param value */ export type ExampleMessage = ParsePsdMessage | DonwloadFileMessage | ErrorMessage ... export function validateMessage(data: unknown): asserts data is ExampleMessage { if ( !( typeof data === 'object' && data !== null && 'type' in data && 'value' in data && (data as ExampleMessage)['signature'] === SIGNATURE ) ) { throw new TypeError(`data is not an ExampleMessage (got ${data})`) } // Check if the "type" field contains known message types const type = (data as ExampleMessage).type switch (type) { case 'ParseData': case 'DownloadFile': case 'Error': ... // These are valid, so pass return default: // Will fail type check if switch statement is non-exhaustive ((value: never) => { throw new TypeError(`Unexpected ExampleMessage type: ${value}`) })(type) } } export function createMessage<Type extends ExampleMessage['type']>( type: Type, value: Extract<ExampleMessage, { type: Type }>['value'], ): Extract<ExampleMessage, { type: Type }> { return { type, value, signature: SIGNATURE, timestamp: Date.now(), } as Extract<ExampleMessage, { type: Type }> }
3. Type error 해결하기
AS IS
ts는 브라우저에서 바로 사용이 불가능해서 js로 컴파일 하는 과정이 필요하다.
그래서 아래와 같이 넣고 실행하면 타입 에러가 날거다.
const myWorker = new Worker("worker.ts");
TO BE
아래처럼 useRef로 만들어주고 current에 Worker를 만들어준다.
이때 경로는 상대경로를 넣어주고, 모듈의 metadata중 url을 같이 넣어준다.
psd 파일을 읽어서 element에 이미지를 뿌려줘야 해서 callback에 엘리먼트도 넣어주었다.
const workerRef = useRef<Worker>() useEffect(() => { workerRef.current = new Worker( new URL('/src/app/worker.ts', import.meta.url), ) workerRef.current.addEventListener('message', (e: MessageEvent<any>) => { workerCallback(e, [targetEl, sourceEl]) }) }, [])
4. workerCallback 생성하기
const workerCallback = ( { data }: MessageEvent<any>, element: HTMLDivElement[], ) => { const { type, timestamp, value } = data validateMessage(data) // type 별로 분기하기 }
5. woker 호출하기
react-dropzone을 사용했고, onDrop에 파일을 업로드했을 때 postMessage를 보내게 설정했다.
const { getRootProps, getInputProps } = useDropzone({ multiple: false, accept: { 'image/vnd.adobe.photoshop': ['.psd', '.psb'], }, onDrop: async (acceptedFiles: File[]) => { const fileExtension = acceptedFiles[0].name.split('.').pop() ?? null if (workerRef.current) { if (fileExtension === 'psd' || fileExtension === 'psb') { readFileAsArrayBuffer(acceptedFiles[0]).then(buffer => { workerRef.current?.postMessage(createMessage('ParseData', buffer), [ buffer, ]) }) } } } } )
6. Transferable 확인하기
Worker의 postMessage가 바로 데이터가 공유되지 않고 복사된다는 점과 몇 가지 타입들은 지원되지 않는다는 점이다. 즉, 복사에 대한 오버헤드도 있다는 걸 염두에 둘 필요가 있으며, Function이나 Dom node 같은 경우는 전송이 불가하고 object의 경우 property 들의 기본 형태만 복사될 뿐 메타데이터 성격의 요소들은 모두 배제되기 때문에 class instance 가 제대로 deserialize 되지 않는 등의 문제가 있었다. 따라서 파일을 그대로 보내면 안되고, 아래 Transferable 형식에 맞게 보내줘야 메타 데이터가 유지 된다. ag-psd를 사용해서 psd를 읽었기 때문에 ArrayBuffer로 바꾸어서 woker에서 동작하게 해주었다.
interface Worker extends EventTarget, AbstractWorker { ... postMessage(message: any, transfer: Transferable[]): void; ... } type Transferable = OffscreenCanvas | ImageBitmap | MessagePort | ReadableStream | WritableStream | TransformStream | VideoFrame | ArrayBuffer;
다음은 ArrayBuffer로 바꾸는 함수이다.
const readFileAsArrayBuffer = (file: File) => { if (file.arrayBuffer) { return file.arrayBuffer() } else { const reader = new FileReader() reader.readAsArrayBuffer(file) return new Promise<ArrayBuffer>(resolve => { reader.addEventListener('load', event => { if (event.target) { resolve(event.target.result as ArrayBuffer) } else { throw new Error('Loaded file but event.target is null') } }) }) } }
소회
이전에는 파일을 부르는데 한 세월, export할 때 또 한 세월이었는데 Web worker를 통해 대용량 파일을 메인 쓰레드 차단 없이 백그라운드에서 처리하게 개선되어서 기분이 좋았다. 또 worker를 통해 timer와 layout을 따로 그려주는 코드도 넣어서 기다리는 시간이 지루하지 않게 UX 개선도 하였다. 여러모로 많은 것을 얻어가는 미니 프로젝트였던 것 같다.
git : https://github.com/issol/psd-translation-tool
demo : https://psd-translation-tool.vercel.app/
'Nextjs' 카테고리의 다른 글
Recoil 사용해서 Modal 여러개 띄우기 (0) 2024.01.30 Input type이 number일 때 maxLength 지정안될 때 (0) 2024.01.30 사용자의 페이지 이탈 감지하기 (1) 2024.01.30