[상] React에서 Wasm과 JS 연산속도 비교하기

youseock·2024년 2월 29일

[JS] WebAssembly

목록 보기
3/4
post-thumbnail

🎯 수도쿠 문제를 Wasm과 자바스크립트로 각각 풀며, 성능 차이를 비교합니다.

해당 글에서는 서브 스레드에서 로직을 실행할 수 있게 돕는 worker에 대해서 알아보고, worker를 사용하여 js로 수도쿠 문제를 풀고, 푼 결과를 브라우저에 반영하는 과정을 담고 있습니다.

왜 Worker를 사용할까 ?

  • Wasm과 일반 자바스크립트의 성능 차이를 확인하기 위해 16x16 크기의 수도쿠 문제를 백트래킹으로 풀고 있다.
  • 해당 연산은 실제로 많은 시간이 걸리기 때문에 메인 쓰레드에서 실행할 경우 웹 앱이 멈춘다.
  • 해당 연산을 웹 워커를 이용해 서브 스레드에서 실행하자.

Worker 란?

  • 프로세스나 스레드와 같은 실행 단위를 뜻한다.
  • 웹 개발에서는 브라우저나 Node.js 환경에서 스크립트를 병렬로 실행하기 위해 사용된다.
    • 브라우저는 webWorker라는 웹 API를 이용하여 워커를 구축한다.
    • Node.js 환경에서는 worker_threads 모듈을 사용하여 워커를 구축한다.
  • 계산 집약적인 작업을 서브 스레드에서 실행하게 도와줘 메인 스레드의 블로킹을 방지할 수 있다.

💡 웹 프론트엔드 개발자의 관점에서 워커란?

현대 웹 앱은 유저와 수많은 상호작용이 있고, 그에 따른 UI 업데이트가 즉시 일어나야 한다. 그렇기 때문에 UI를 변경하는 메인 쓰레드는 유휴 상태로 유지하는 것이 중요하다.

메인 쓰레드를 유휴 상태로 유지하기 위해 모든 js 로직을 워커로 실행하자는 말은 아니다. 워커를 사용하는 데에도 비용이 든다.

  1. 워커는 일부 호스트 API에 접근이 불가능하다.
  2. 서브 스레드와 메인 스레드 간의 통신 비용이 든다.

결국 CPU 집약적이거나 오랜 시간이 소요되는 작업만 워커로 실행하는 편이 합리적이라 생각한다.

Worker의 특징

  1. 워커는 이벤트 기반 아키텍처를 따르며, 이를 통해 비동기적인 작업을 처리하고 메인 스레드와 효율적으로 상호작용할 수 있다.

  2. 워커 스크립트는 export하지 않는다. 워커는 주로 백그라운드에서 비동기적으로 실행되기 때문에 모듈을 내보내지 않고 사용한다. 대신 메인 스레드와 통신을 위해 postMessage, onmessage를 사용한다.

worker_threads 모듈을 사용한 예시

main.js

const { Worker } = require("worker_threads");
const worker = new Worker("./worker.js");

// 워커 이벤트 핸들러 등록
worker.on("message", (value) => {
  console.log(`부모[수신] : ${value}`);
});

worker.on("exit", () => {
  console.log("부모[수신] : 워커 종료 이벤트");
  console.log("");
});

// 워커에게 메시지 보내기
const msg = "워커야 이 메시지 받을 수 있어 ?";
console.log(`부모[송신] : ${msg}`);
worker.postMessage(msg);

worker.js

const { parentPort } = require("worker_threads");

parentPort.on("message", (value) => {
  console.log(`자식[수신] : ${value}`);

  const msg = `부모님, 이 메시지 받으셨나요?`;
  console.log(`자식[송신] : ${msg}`);
  parentPort.postMessage(msg);

  console.log("자식[송신] : 워커 종료 이벤트");
  parentPort.close();
});

  • 3 ~ 4 번째 줄 순서가 바뀐 채 콘솔에 찍히는데, 이는 자바스크립트의 비동기 특성 때문이다. 콘솔에 출력되는 순서와 실제 코드의 실행 순서가 항상 일치하지 않을 수 있다.
  • 이를 검증하기 위해 호출될 때 마다 1을 더 하는 로직을 추가하면 송신이 먼저 일어난다는 사실을 확인할 수 있다.

리액트에서 Worker 사용하기

  • 사용한 기술 스택 : react, vite
  • 웹 워커는 다른 스레드와 통신하기 위해 postMessage API를 제공한다. comlink는 이 메시지 기반 API를 RPC 구현을 제공하여 한 스레드에서 작동하는 객체를 다른 스레드에서도 마치 로컬 객체처럼 사용할 수 있게 돕는다.
pnpm i comlink
pnpm i -D vite-plugin-comlink
  • 사용하시는 패키지에 맞춰 npm이나 yarn을 사용하세요!

vite 설정하기

vite.config.ts

export default defineConfig({
  plugins: [react(), comlink()],
  worker: {
    plugins: () => [comlink()],
  },
});

vite-env.d.ts

/// <reference types="vite/client" />
/// <reference types="vite-plugin-comlink/client" /> 👈👈👈

Worker Instance 생성하고 리액트 컴포넌트에서 사용하기

worker.ts

export const workerInstance = new ComlinkWorker<
  typeof import("수행할_작업이_있는_곳")
>(new URL("수행할_작업이_있는_곳", import.meta.url));

sudokuSolve.ts

export const sudokuSolve = (initialBoard: Board) => {
	// ...
	return copyBoard;
}

sudoku.tsx

import { workerInstance } from "../../worker/js";
...
useEffect(() => {
	(async function (){
			workerInstance.sudokuSolve(board);
	})();	
},[])
return (...)
  • 생성한 워커 인스턴스를 import한 후에 link 시켜놓은 작업을 수행시킬 수 있다.
  • 당연히 메인 스레드에서 돌아가지 않기 때문에 서브 스레드에서 계산한 결과를 화면에 반영 시키기 위해 비동기 작업을 수행해야 한다.
  • workerInstance.수행함수의 리턴 값이 Promise이기 때문에 .then이나 async로 후속 작업을 할 수 있다.
  • 자세한 코드는 여기서 확인할 수 있습니다.

결과

  • dom 조작을 위해 0.2s 딜레이를 줬습니다.
profile
자바스크립트 애호가

0개의 댓글