JavaScript로는 한계다: Rust + WebAssembly로 브라우저 성능을 10배 끌어올리기

프론트엔드

WebAssemblyWASMRust성능 최적화브라우저

이 글은 누구를 위한 것인가

  • 브라우저에서 무거운 계산 작업(이미지 처리, 암호화, 대용량 데이터 파싱)을 다루는 개발자
  • WebAssembly(WASM)가 무엇인지 궁금했지만 어렵게 느껴진 분
  • Figma나 AutoCAD 같은 성능 집약적 웹 앱이 어떻게 만들어지는지 궁금한 분

들어가며

Figma는 웹 앱이다. 브라우저에서 실행되는데, 복잡한 벡터 그래픽을 실시간으로 렌더링하고 수백 개의 레이어를 부드럽게 처리한다. 어떻게 이게 가능할까?

답은 WebAssembly다. Figma의 렌더링 엔진 핵심은 C++로 작성되어 WebAssembly로 컴파일됐다. 브라우저에서 JavaScript가 아닌 기계어에 가까운 코드가 실행되기 때문에 이런 성능이 가능하다.

AutoCAD, Google Earth, Adobe Photoshop(웹 버전)도 같은 방식이다. WebAssembly가 브라우저 성능의 한계를 어떻게 돌파하는지, 그리고 우리도 어떻게 활용할 수 있는지 알아보자.


1. WebAssembly가 무엇인가

WebAssembly(WASM)는 브라우저에서 실행되는 바이너리 형식의 코드다. C, C++, Rust 같은 저수준 언어로 작성된 코드를 컴파일해서 브라우저에서 실행할 수 있다.

[기존 방식]
C/C++/Rust 코드 → 운영체제 실행 파일 → 네이티브 앱

[WebAssembly 방식]
C/C++/Rust 코드 → .wasm 파일 → 브라우저에서 실행

JavaScript와 비교하면:

항목JavaScriptWebAssembly
형식텍스트 소스코드바이너리
파싱런타임에 파싱+컴파일미리 컴파일됨, 빠른 로드
타입동적 타입정적 타입 (강한 최적화)
계산 성능기준2~20배 빠름 (작업에 따라)
DOM 접근직접 가능JavaScript 통해야 함
사용 용도범용계산 집약적 작업 특화

WebAssembly는 JavaScript를 대체하는 것이 아니다. 성능이 중요한 특정 연산을 JavaScript 대신 처리하고, DOM 조작 등 나머지는 JavaScript가 담당한다.


2. WebAssembly가 실제로 쓰이는 곳

Figma: 벡터 그래픽 렌더링 엔진 (C++ → WASM) AutoCAD Web: 2D/3D CAD 렌더링 (C++ → WASM) Google Earth: 지구 렌더링 (C++ → WASM) Adobe Photoshop (웹): 픽셀 연산 (C++ → WASM) TensorFlow.js: ML 모델 추론 가속 (C++ → WASM) ffmpeg.wasm: 브라우저에서 동영상 변환 (C → WASM)

특징을 보면 모두 계산 집약적인 작업이다. 이미지/비디오 처리, 3D 렌더링, ML 추론 — JavaScript로는 너무 느린 작업들이다.


3. Rust를 선택하는 이유

C, C++, Rust 모두 WebAssembly로 컴파일할 수 있다. 그런데 웹 개발에서 Rust가 선호되는 이유가 있다.

메모리 안전성: Rust는 컴파일 타임에 메모리 오류를 잡아낸다. C/C++에서 흔한 메모리 누수, 버퍼 오버플로우가 Rust에서는 컴파일 오류로 잡힌다.

wasm-pack 생태계: Rust에는 WebAssembly 빌드를 쉽게 만들어주는 wasm-pack이 있다.

wasm-bindgen: JavaScript ↔ Rust 바인딩을 자동으로 생성해준다.


4. Rust + WASM 기본 설정

개발 환경 설정

# Rust 설치
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# WebAssembly 타겟 추가
rustup target add wasm32-unknown-unknown

# wasm-pack 설치
cargo install wasm-pack

첫 번째 WASM 모듈 만들기

# Rust 라이브러리 프로젝트 생성
cargo new --lib wasm-image-processor
cd wasm-image-processor
# Cargo.toml
[package]
name = "wasm-image-processor"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"
// src/lib.rs
use wasm_bindgen::prelude::*;

// JavaScript에서 호출 가능한 함수
#[wasm_bindgen]
pub fn grayscale(pixels: &[u8]) -> Vec<u8> {
    let mut result = Vec::with_capacity(pixels.len());

    // RGBA 픽셀 처리
    for chunk in pixels.chunks(4) {
        let r = chunk[0] as f32;
        let g = chunk[1] as f32;
        let b = chunk[2] as f32;
        let a = chunk[3];

        // 그레이스케일 변환 (표준 공식)
        let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;

        result.push(gray);
        result.push(gray);
        result.push(gray);
        result.push(a);
    }

    result
}

#[wasm_bindgen]
pub fn add(a: u32, b: u32) -> u32 {
    a + b // JavaScript에서 호출해 테스트
}

빌드

wasm-pack build --target web

빌드 결과물:

pkg/
  ├── wasm_image_processor.js    # JavaScript 바인딩 (자동 생성)
  ├── wasm_image_processor_bg.wasm  # WASM 바이너리
  └── package.json

5. JavaScript에서 WASM 모듈 사용하기

<!-- index.html -->
<canvas id="canvas"></canvas>
<input type="file" id="imageInput" accept="image/*">

<script type="module">
import init, { grayscale } from './pkg/wasm_image_processor.js';

// WASM 초기화
await init();

document.getElementById('imageInput').addEventListener('change', async (e) => {
  const file = e.target.files[0];
  const imageBitmap = await createImageBitmap(file);

  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');
  canvas.width = imageBitmap.width;
  canvas.height = imageBitmap.height;
  ctx.drawImage(imageBitmap, 0, 0);

  // 이미지 픽셀 데이터 가져오기
  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

  // WASM으로 그레이스케일 처리 (이 연산이 JS보다 훨씬 빠름)
  const grayPixels = grayscale(imageData.data);

  // 결과를 캔버스에 적용
  const resultImageData = new ImageData(
    new Uint8ClampedArray(grayPixels),
    canvas.width,
    canvas.height
  );
  ctx.putImageData(resultImageData, 0, 0);
});
</script>

6. JS ↔ WASM 데이터 교환과 성능 주의사항

JavaScript와 WASM 사이의 데이터 전달에는 복사 비용이 든다.

// 큰 데이터를 복사 없이 전달하는 방법
#[wasm_bindgen]
pub fn process_in_place(data: &mut [u8]) {
    // 데이터를 복사하지 않고 원본 수정
    for i in 0..data.len() {
        data[i] = data[i].saturating_add(10);
    }
}
// SharedArrayBuffer로 복사 없이 공유 (Web Workers와 함께)
const sharedBuffer = new SharedArrayBuffer(imageData.length);
const sharedArray = new Uint8Array(sharedBuffer);
sharedArray.set(imageData.data);

// WASM이 같은 메모리 사용 (복사 없음)
processInPlace(sharedArray);

WASM 사용이 효과적인 케이스

적합부적합
반복적인 수치 계산 (1000번 이상)한 번만 실행하는 간단한 계산
이미지/오디오/비디오 처리DOM 조작
암호화, 해시 계산네트워크 요청
대용량 데이터 파싱단순 문자열 처리

7. 실제 성능 비교: JavaScript vs WebAssembly

1920×1080 이미지(약 800만 픽셀)의 그레이스케일 변환 성능:

방법처리 시간
JavaScript (순수)450ms
JavaScript + 최적화120ms
WebAssembly (Rust)35ms
WebAssembly + SIMD8ms

**SIMD(Single Instruction Multiple Data)**는 하나의 명령으로 여러 픽셀을 동시에 처리하는 기법이다. WebAssembly SIMD가 활성화되면 성능이 추가로 4~8배 향상된다.


8. WASM을 도입할 때 vs 도입하지 말아야 할 때

도입 권장

  • 이미지 필터, 리사이징, 압축
  • 클라이언트 사이드 암호화 (비밀번호 해싱, 데이터 암호화)
  • PDF/엑셀 파싱 및 생성
  • ML 추론 (소형 모델)
  • 게임 엔진, 물리 시뮬레이션

도입 불필요

  • 단순 폼 검증, API 호출
  • React/Vue 컴포넌트 로직
  • 애니메이션 (CSS가 더 적합)
  • 빈번하지 않은 1회성 계산

결정 기준: JavaScript 프로파일링 결과 특정 함수가 전체 실행 시간의 20% 이상을 차지하고, 반복적으로 실행된다면 WASM 대체를 고려하라.


맺으며

WebAssembly는 브라우저의 새로운 가능성을 열었다. 10년 전에는 "브라우저에서는 이런 건 불가능해"라고 했던 것들이 이제 Figma, Photoshop 웹 버전처럼 실제로 동작한다.

모든 웹 앱이 WASM을 필요로 하지는 않는다. 하지만 이미지 처리, 암호화, 대용량 데이터 처리처럼 "JavaScript로는 너무 느리다"는 문제를 만났다면, WASM이 실용적인 해결책이 될 수 있다.

Rust를 배우지 않아도 시작할 수 있다. ffmpeg.wasm, @wasmer/wasi 같은 기존 WASM 라이브러리를 사용해보는 것이 첫 걸음이다. 성능 개선의 맛을 보고 나면, 직접 Rust로 WASM 모듈을 만드는 것도 도전해볼 만하다.