Rust로 WebAssembly 만들기 — wasm-pack 실전 가이드
이미지 처리를 JS에서 WASM으로 옮기니 20배 빨라졌다
2년 전, 우리 서비스의 이미지 처리 파이프라인이 너무 느렸다. 각 이미지마다 2-3초가 걸렸다. JavaScript로는 한계였다.
Rust + WebAssembly로 옮겼다. 결과? 100ms로 떨어졌다.
이 글은 Rust를 WASM으로 컴파일하고, 브라우저에서 사용하는 전 과정이다.
설치 및 설정
필요한 도구:
# Rust 설치 (이미 있다고 가정)
# WebAssembly target 추가
rustup target add wasm32-unknown-unknown
# wasm-pack 설치
curl https://rustwasm.org/wasm-pack/installer/init.sh -sSf | sh
새 프로젝트 생성:
cargo new --lib image-processor
cd image-processor
# Cargo.toml 수정
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
image = "0.24"
[profile.release]
opt-level = "z" # 최소 크기
lto = true
wasm-bindgen 기본
wasm-bindgen은 Rust와 JavaScript 사이의 다리 역할을 한다:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
#[wasm_bindgen]
pub fn add(a: u32, b: u32) -> u32 {
a + b
}
컴파일:
wasm-pack build --target web --release
JavaScript에서 사용:
import * as wasm from './image_processor.js';
console.log(wasm.greet("World")); // "Hello, World!"
console.log(wasm.add(5, 3)); // 8
실제 예제: 이미지 처리
use wasm_bindgen::prelude::*;
use image::{ImageBuffer, Rgba, RgbaImage};
#[wasm_bindgen]
pub struct ImageProcessor {
width: u32,
height: u32,
data: Vec<u8>,
}
#[wasm_bindgen]
impl ImageProcessor {
#[wasm_bindgen(constructor)]
pub fn new(width: u32, height: u32) -> ImageProcessor {
let size = (width * height * 4) as usize;
ImageProcessor {
width,
height,
data: vec![0; size],
}
}
#[wasm_bindgen]
pub fn load_image(&mut self, pixels: &[u8]) {
self.data = pixels.to_vec();
}
#[wasm_bindgen]
pub fn grayscale(&mut self) {
for i in (0..self.data.len()).step_by(4) {
let r = self.data[i] as f32;
let g = self.data[i + 1] as f32;
let b = self.data[i + 2] as f32;
let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
self.data[i] = gray;
self.data[i + 1] = gray;
self.data[i + 2] = gray;
}
}
#[wasm_bindgen]
pub fn get_data(&self) -> Vec<u8> {
self.data.clone()
}
}
JavaScript 사용:
import * as wasm from './image_processor.js';
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// 이미지 로드
const img = new Image();
img.src = 'test.jpg';
img.onload = () => {
// Canvas에 그리기
ctx.drawImage(img, 0, 0);
// ImageData 추출
const imageData = ctx.getImageData(0, 0, img.width, img.height);
// WASM 함수 호출
const processor = new wasm.ImageProcessor(img.width, img.height);
processor.load_image(imageData.data);
processor.grayscale();
// 결과를 Canvas에 그리기
const result = processor.get_data();
const resultData = ctx.createImageData(img.width, img.height);
resultData.data.set(new Uint8Array(result));
ctx.putImageData(resultData, 0, 0);
};
성능 비교
1000x1000 이미지에서 grayscale 필터 적용 시간:
- JavaScript: 250ms
- Rust + WASM: 12ms
- 성능 향상: 20배
바이너리 크기:
- 원본 WASM: 2.3MB
- 최적화 후: 280KB (10배 작음)
- gzip 압축: 85KB
WASM 파일 크기 최적화
방법 1: 릴리스 프로필
[profile.release]
opt-level = "z" # 크기 최적화
lto = true # 링크 타임 최적화
codegen-units = 1 # 최대 최적화
방법 2: wasm-opt 사용
npm install -g wasm-opt
wasm-opt -Oz -o output.wasm input.wasm
방법 3: 필요한 기능만 빌드
// cargo.toml
[dependencies.image]
version = "0.24"
default-features = false
features = ["jpeg"] # 필요한 것만
메모리 관리
WASM과 JavaScript 사이의 메모리 전달이 중요하다:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct Buffer {
// Vec는 자동으로 관리됨
data: Vec<u8>,
}
#[wasm_bindgen]
impl Buffer {
#[wasm_bindgen(constructor)]
pub fn new(size: usize) -> Buffer {
Buffer {
data: vec![0; size],
}
}
// 메모리 직접 접근 (고급)
#[wasm_bindgen]
pub fn as_ptr(&self) -> *const u8 {
self.data.as_ptr()
}
}
브라우저 vs Node.js
브라우저:
wasm-pack build --target web
Node.js:
wasm-pack build --target nodejs
유니버설 (둘 다):
wasm-pack build --target bundler
실제 프로덕션 설정
// 전체 wasm-pack.toml
[package]
name = "image-processor"
version = "0.1.0"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
image = { version = "0.24", default-features = false, features = ["jpeg", "png"] }
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
strip = true
빌드 스크립트:
#!/bin/bash
wasm-pack build --release --target web
wasm-opt -Oz -o pkg/image_processor_bg.wasm pkg/image_processor_bg.wasm
gzip -k pkg/*.wasm
디버깅
WASM 코드를 디버깅하려면:
wasm-pack build --dev # 디버그 정보 포함
// JavaScript에서
console.log("WASM result:", wasm.process_image(data));
결론
Rust + WASM은 성능이 중요한 웹 애플리케이션에 최적이다:
- CPU 바운드 작업: 10-100배 빨라짐
- 복잡한 알고리즘: 매우 빠름
- 이미지/비디오 처리: 매우 추천
하지만:
- I/O 바운드 작업은 JavaScript와 차이 없음
- 작은 작업은 오버헤드가 더 클 수 있음
- 개발 시간이 더 걸림
오랜 개발 경험에서 드리는 조언: WASM은 "은 탄환"이 아니다. 진짜 필요한 곳에만 쓰자.