Rust로 WebAssembly 만들기 — wasm-pack 실전 가이드

게시일: 2025년 10월 7일 · 15분 읽기

이미지 처리를 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 필터 적용 시간:

바이너리 크기:

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은 성능이 중요한 웹 애플리케이션에 최적이다:

하지만:

오랜 개발 경험에서 드리는 조언: WASM은 "은 탄환"이 아니다. 진짜 필요한 곳에만 쓰자.

iL
ian.lab

실무 개발자입니다. 현장에서 겪은 문제와 해결 과정을 기록합니다. 오류 제보는 연락처로 보내주세요.