WebAssembly 실전 가이드 | C++/Rust를 웹에서 실행하는 방법
이 글의 핵심
WebAssembly로 C++/Rust 코드를 브라우저에서 네이티브 수준 성능으로 실행하는 방법. Emscripten, wasm-pack 사용법, JavaScript 연동, 최적화 기법을 실전 예제와 함께 설명합니다.
들어가며
WebAssembly(WASM)는 C++, Rust 같은 네이티브 언어를 브라우저에서 거의 네이티브 수준 성능으로 실행할 수 있게 해주는 바이너리 포맷입니다. 게임 엔진, 이미지/비디오 처리, 암호화, 과학 계산 등 CPU 집약적 작업을 웹에서 구현할 때 JavaScript의 한계를 넘어설 수 있습니다.
이 글은 C++과 Rust 코드를 WASM으로 컴파일하는 방법, JavaScript와 연동하는 방법, 성능 최적화 기법, 실무 사례를 단계별로 설명합니다.
목차
- WebAssembly란?
- C++로 WASM 만들기 (Emscripten)
- Rust로 WASM 만들기 (wasm-pack)
- JavaScript 연동
- 성능 최적화
- 실무 사례
- 트러블슈팅
- 마무리
WebAssembly란?
핵심 특징
1. 바이너리 포맷
- 텍스트 형식
.wat(WebAssembly Text) - 바이너리 형식
.wasm(실제 배포 형식) - 파싱·컴파일 속도가 JavaScript보다 빠름
2. 샌드박스 실행
- 브라우저 보안 모델 내에서 안전하게 실행
- 메모리는 선형 배열(Linear Memory)로 격리
- 직접 DOM 접근 불가 (JavaScript 통해 간접 접근)
3. 언어 독립적
- C, C++, Rust, Go, AssemblyScript 등 다양한 언어 지원
- LLVM 기반 컴파일러 체인 활용
아키텍처
┌─────────────┐
│ C++/Rust │
│ Source Code │
└──────┬──────┘
│ Compile
▼
┌─────────────┐
│ .wasm │
│ Binary │
└──────┬──────┘
│ Load
▼
┌─────────────┐ ┌──────────────┐
│ JavaScript │◄───►│ WASM Module │
│ Glue Code │ │ (Sandbox) │
└─────────────┘ └──────────────┘
│
▼
┌─────────────┐
│ Browser │
│ (DOM/API) │
└─────────────┘
C++로 WASM 만들기 (Emscripten)
Emscripten 설치
# Emscripten SDK 설치
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh # Windows: emsdk_env.bat
기본 예제: 수학 함수
hello.cpp
#include <emscripten/emscripten.h>
#include <cmath>
// EMSCRIPTEN_KEEPALIVE: JavaScript에서 호출 가능하도록 export
extern "C" {
EMSCRIPTEN_KEEPALIVE
int add(int a, int b) {
return a + b;
}
EMSCRIPTEN_KEEPALIVE
double calculateDistance(double x1, double y1, double x2, double y2) {
double dx = x2 - x1;
double dy = y2 - y1;
return std::sqrt(dx * dx + dy * dy);
}
EMSCRIPTEN_KEEPALIVE
void processArray(int* arr, int size) {
for (int i = 0; i < size; i++) {
arr[i] = arr[i] * 2;
}
}
}
컴파일
# 기본 컴파일
emcc hello.cpp -o hello.js \
-s EXPORTED_FUNCTIONS='["_add","_calculateDistance","_processArray"]' \
-s EXPORTED_RUNTIME_METHODS='["ccall","cwrap"]'
# 최적화 빌드
emcc hello.cpp -o hello.js \
-O3 \
-s EXPORTED_FUNCTIONS='["_add","_calculateDistance","_processArray"]' \
-s EXPORTED_RUNTIME_METHODS='["ccall","cwrap"]' \
-s MODULARIZE=1 \
-s EXPORT_NAME='createModule' \
--closure 1
컴파일 옵션 설명:
-O3: 최대 최적화 (파일 크기와 성능 균형)-s MODULARIZE=1: ES6 모듈로 export--closure 1: Google Closure Compiler로 추가 압축-s WASM=1: WASM 출력 (기본값)
JavaScript에서 호출
// hello.js와 hello.wasm 로드
const Module = await createModule();
// 함수 호출
const result = Module.ccall('add', 'number', ['number', 'number'], [10, 20]);
console.log(result); // 30
// 또는 cwrap으로 래핑
const add = Module.cwrap('add', 'number', ['number', 'number']);
console.log(add(15, 25)); // 40
// 배열 처리
const size = 5;
const ptr = Module._malloc(size * 4); // int = 4 bytes
const arr = new Int32Array(Module.HEAP32.buffer, ptr, size);
arr.set([1, 2, 3, 4, 5]);
Module.ccall('processArray', null, ['number', 'number'], [ptr, size]);
console.log(Array.from(arr)); // [2, 4, 6, 8, 10]
Module._free(ptr); // 메모리 해제 필수!
이미지 처리 예제
image_processor.cpp
#include <emscripten/emscripten.h>
#include <cstdint>
extern "C" {
EMSCRIPTEN_KEEPALIVE
void grayscale(uint8_t* pixels, int width, int height) {
for (int i = 0; i < width * height; i++) {
int offset = i * 4; // RGBA
uint8_t r = pixels[offset];
uint8_t g = pixels[offset + 1];
uint8_t b = pixels[offset + 2];
// 그레이스케일 변환
uint8_t gray = static_cast<uint8_t>(0.299 * r + 0.587 * g + 0.114 * b);
pixels[offset] = gray;
pixels[offset + 1] = gray;
pixels[offset + 2] = gray;
// Alpha 채널은 유지
}
}
EMSCRIPTEN_KEEPALIVE
void blur(uint8_t* pixels, int width, int height, int radius) {
// 간단한 박스 블러 구현
// 실제로는 가우시안 블러나 최적화된 알고리즘 사용
// ... (구현 생략)
}
}
emcc image_processor.cpp -o image_processor.js \
-O3 \
-s EXPORTED_FUNCTIONS='["_grayscale","_blur"]' \
-s EXPORTED_RUNTIME_METHODS='["ccall","cwrap"]' \
-s MODULARIZE=1 \
-s EXPORT_NAME='ImageProcessor' \
-s ALLOW_MEMORY_GROWTH=1
JavaScript 연동:
const processor = await ImageProcessor();
// Canvas에서 이미지 데이터 가져오기
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// WASM 메모리로 복사
const ptr = processor._malloc(imageData.data.length);
processor.HEAPU8.set(imageData.data, ptr);
// 그레이스케일 처리
processor.ccall('grayscale', null,
['number', 'number', 'number'],
[ptr, canvas.width, canvas.height]
);
// 결과를 다시 Canvas로
const processed = new Uint8ClampedArray(
processor.HEAPU8.buffer, ptr, imageData.data.length
);
imageData.data.set(processed);
ctx.putImageData(imageData, 0, 0);
processor._free(ptr);
Rust로 WASM 만들기 (wasm-pack)
환경 설정
# Rust 설치 (rustup.rs)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# wasm-pack 설치
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# 프로젝트 생성
cargo new --lib my-wasm-project
cd my-wasm-project
Cargo.toml 설정:
[package]
name = "my-wasm-project"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
기본 예제
src/lib.rs
use wasm_bindgen::prelude::*;
// JavaScript에서 호출 가능한 함수
#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u64 {
match n {
0 => 0,
1 => 1,
_ => {
let mut a = 0u64;
let mut b = 1u64;
for _ in 2..=n {
let temp = a + b;
a = b;
b = temp;
}
b
}
}
}
// 구조체 export
#[wasm_bindgen]
pub struct Calculator {
value: f64,
}
#[wasm_bindgen]
impl Calculator {
#[wasm_bindgen(constructor)]
pub fn new() -> Calculator {
Calculator { value: 0.0 }
}
pub fn add(&mut self, x: f64) {
self.value += x;
}
pub fn multiply(&mut self, x: f64) {
self.value *= x;
}
pub fn get_value(&self) -> f64 {
self.value
}
pub fn reset(&mut self) {
self.value = 0.0;
}
}
// console.log 사용
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
#[wasm_bindgen]
pub fn greet(name: &str) {
log(&format!("Hello, {}!", name));
}
빌드
# 개발 빌드
wasm-pack build --target web
# 프로덕션 빌드 (최적화)
wasm-pack build --target web --release
# npm 패키지로 빌드
wasm-pack build --target bundler --release
JavaScript에서 사용
// ES6 모듈로 import
import init, { add, fibonacci, Calculator, greet } from './pkg/my_wasm_project.js';
async function run() {
// WASM 초기화
await init();
// 함수 호출
console.log(add(10, 20)); // 30
console.log(fibonacci(10)); // 55
// 구조체 사용
const calc = new Calculator();
calc.add(10);
calc.multiply(2);
console.log(calc.get_value()); // 20
calc.free(); // 메모리 해제
// console.log 호출
greet('WASM'); // "Hello, WASM!"
}
run();
고급 예제: 이미지 필터
src/lib.rs
use wasm_bindgen::prelude::*;
#[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],
}
}
pub fn get_data_ptr(&self) -> *const u8 {
self.data.as_ptr()
}
pub fn set_pixel_data(&mut self, data: &[u8]) {
self.data.copy_from_slice(data);
}
pub fn grayscale(&mut self) {
for i in 0..(self.width * self.height) as usize {
let offset = i * 4;
let r = self.data[offset] as f32;
let g = self.data[offset + 1] as f32;
let b = self.data[offset + 2] as f32;
let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
self.data[offset] = gray;
self.data[offset + 1] = gray;
self.data[offset + 2] = gray;
}
}
pub fn invert(&mut self) {
for i in 0..(self.width * self.height) as usize {
let offset = i * 4;
self.data[offset] = 255 - self.data[offset];
self.data[offset + 1] = 255 - self.data[offset + 1];
self.data[offset + 2] = 255 - self.data[offset + 2];
}
}
pub fn brightness(&mut self, factor: f32) {
for i in 0..(self.width * self.height) as usize {
let offset = i * 4;
self.data[offset] = ((self.data[offset] as f32 * factor).min(255.0)) as u8;
self.data[offset + 1] = ((self.data[offset + 1] as f32 * factor).min(255.0)) as u8;
self.data[offset + 2] = ((self.data[offset + 2] as f32 * factor).min(255.0)) as u8;
}
}
}
JavaScript 사용:
import init, { ImageProcessor } from './pkg/my_wasm_project.js';
async function processImage() {
await init();
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// ImageProcessor 생성
const processor = new ImageProcessor(canvas.width, canvas.height);
// 이미지 데이터 복사
processor.set_pixel_data(imageData.data);
// 필터 적용
processor.grayscale();
// processor.invert();
// processor.brightness(1.5);
// 결과를 Canvas로
const ptr = processor.get_data_ptr();
const processed = new Uint8ClampedArray(
wasm.memory.buffer, ptr, imageData.data.length
);
imageData.data.set(processed);
ctx.putImageData(imageData, 0, 0);
processor.free();
}
JavaScript 연동
데이터 전달 방식
1. 숫자 (Number)
#[wasm_bindgen]
pub fn process_number(x: f64) -> f64 {
x * 2.0
}
const result = process_number(3.14); // 6.28
2. 문자열 (String)
#[wasm_bindgen]
pub fn reverse_string(s: &str) -> String {
s.chars().rev().collect()
}
const reversed = reverse_string("hello"); // "olleh"
3. 배열 (Array/Vec)
#[wasm_bindgen]
pub fn sum_array(arr: &[i32]) -> i32 {
arr.iter().sum()
}
#[wasm_bindgen]
pub fn create_array(size: usize) -> Vec<i32> {
(0..size as i32).collect()
}
const sum = sum_array(new Int32Array([1, 2, 3, 4, 5])); // 15
const arr = create_array(10); // [0, 1, 2, ..., 9]
4. 객체 (Struct)
#[wasm_bindgen]
pub struct Point {
pub x: f64,
pub y: f64,
}
#[wasm_bindgen]
impl Point {
#[wasm_bindgen(constructor)]
pub fn new(x: f64, y: f64) -> Point {
Point { x, y }
}
pub fn distance(&self, other: &Point) -> f64 {
let dx = self.x - other.x;
let dy = self.y - other.y;
(dx * dx + dy * dy).sqrt()
}
}
const p1 = new Point(0, 0);
const p2 = new Point(3, 4);
console.log(p1.distance(p2)); // 5.0
p1.free();
p2.free();
메모리 관리
수동 메모리 관리 (C++ Emscripten):
// 메모리 할당
const ptr = Module._malloc(1024);
// 사용
const view = new Uint8Array(Module.HEAPU8.buffer, ptr, 1024);
// 해제 (필수!)
Module._free(ptr);
자동 메모리 관리 (Rust wasm-bindgen):
// Rust 객체는 .free() 호출 필요
const obj = new MyRustStruct();
obj.do_something();
obj.free(); // 명시적 해제
// 또는 try-finally
try {
const obj = new MyRustStruct();
obj.do_something();
} finally {
obj.free();
}
성능 최적화
1. 컴파일 최적화
Emscripten (C++):
emcc source.cpp -o output.js \
-O3 \ # 최대 최적화
-s WASM=1 \
-s ALLOW_MEMORY_GROWTH=1 \ # 동적 메모리 증가
-s INITIAL_MEMORY=16MB \ # 초기 메모리
-s MAXIMUM_MEMORY=256MB \ # 최대 메모리
-s STACK_SIZE=1MB \ # 스택 크기
--closure 1 \ # Closure Compiler
-flto \ # Link Time Optimization
-s MODULARIZE=1
wasm-opt (추가 최적화):
# Binaryen wasm-opt 사용
wasm-opt input.wasm -O3 -o output.wasm
# 더 공격적인 최적화
wasm-opt input.wasm -O4 --enable-simd -o output.wasm
Rust:
[profile.release]
opt-level = 3 # 또는 'z' (크기 최적화), 's' (크기 우선)
lto = true # Link Time Optimization
codegen-units = 1 # 단일 코드 생성 유닛 (느리지만 최적)
strip = true # 디버그 심볼 제거
panic = 'abort' # 패닉 시 언와인딩 비활성화
2. SIMD 활용
Rust SIMD 예제:
use std::arch::wasm32::*;
#[wasm_bindgen]
pub fn add_arrays_simd(a: &[f32], b: &[f32]) -> Vec<f32> {
let mut result = Vec::with_capacity(a.len());
unsafe {
let chunks = a.len() / 4;
for i in 0..chunks {
let offset = i * 4;
let va = v128_load(a.as_ptr().add(offset) as *const v128);
let vb = v128_load(b.as_ptr().add(offset) as *const v128);
let vr = f32x4_add(va, vb);
let temp: [f32; 4] = std::mem::transmute(vr);
result.extend_from_slice(&temp);
}
// 나머지 처리
for i in (chunks * 4)..a.len() {
result.push(a[i] + b[i]);
}
}
result
}
3. 멀티스레딩
SharedArrayBuffer 사용:
# 멀티스레딩 지원 빌드
emcc source.cpp -o output.js \
-pthread \
-s USE_PTHREADS=1 \
-s PTHREAD_POOL_SIZE=4
주의사항:
Cross-Origin-Opener-Policy: same-origin헤더 필요Cross-Origin-Embedder-Policy: require-corp헤더 필요- 모든 브라우저에서 지원되지 않음 (보안 정책)
4. 파일 크기 최적화
전략:
-
불필요한 표준 라이브러리 제거
# C++: -fno-exceptions, -fno-rtti emcc source.cpp -o output.js -O3 -fno-exceptions -fno-rtti -
Rust: 작은 할당자 사용
[dependencies] wee_alloc = "0.4"#[global_allocator] static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; -
압축
# Brotli 압축 (gzip보다 15-20% 더 작음) brotli output.wasm -o output.wasm.br -
코드 분할
- 필요한 기능만 별도 WASM 모듈로 분리
- 동적 import로 필요할 때만 로드
실무 사례
1. Figma - 디자인 툴
사용 기술: C++ → WASM (Emscripten)
적용 분야:
- 벡터 그래픽 렌더링 엔진
- 복잡한 레이어 연산
- 실시간 필터 효과
성과: JavaScript 대비 3-10배 빠른 렌더링 성능
2. Google Earth - 3D 지도
사용 기술: C++ → WASM
적용 분야:
- 3D 지형 렌더링
- 대용량 지도 데이터 처리
- 실시간 카메라 제어
3. AutoCAD Web - CAD 소프트웨어
사용 기술: C++ (30년 코드베이스) → WASM
적용 분야:
- 기존 데스크톱 코드 재사용
- 복잡한 CAD 연산
- 대용량 도면 파일 처리
4. Photoshop Web - 이미지 편집
사용 기술: C++ → WASM
적용 분야:
- 이미지 필터 (블러, 샤프닝 등)
- 레이어 합성
- 색상 보정 알고리즘
5. 암호화 라이브러리
사용 기술: Rust → WASM
적용 분야:
- AES, RSA 암호화
- 해시 함수 (SHA-256, SHA-512)
- 서명 검증
장점: 네이티브 수준 성능 + 메모리 안전성
트러블슈팅
문제 1: WASM 파일이 로드되지 않음
증상:
Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "application/octet-stream"
해결:
# nginx 설정
location ~ \.wasm$ {
types {
application/wasm wasm;
}
add_header Content-Type application/wasm;
}
문제 2: 메모리 부족 에러
증상:
RuntimeError: memory access out of bounds
해결:
# Emscripten: 메모리 증가 허용
emcc source.cpp -o output.js \
-s ALLOW_MEMORY_GROWTH=1 \
-s INITIAL_MEMORY=32MB \
-s MAXIMUM_MEMORY=512MB
문제 3: 함수를 찾을 수 없음
증상:
TypeError: Module.myFunction is not a function
해결:
# 함수 export 확인
emcc source.cpp -o output.js \
-s EXPORTED_FUNCTIONS='["_myFunction"]' \
-s EXPORTED_RUNTIME_METHODS='["ccall","cwrap"]'
# C++ 코드에서 extern "C" 확인
extern "C" {
EMSCRIPTEN_KEEPALIVE
void myFunction() { }
}
문제 4: 성능이 기대보다 낮음
원인 분석:
-
JavaScript ↔ WASM 경계 호출이 너무 잦음
- 해결: 배치 처리, 큰 단위로 데이터 전달
-
메모리 복사 오버헤드
- 해결: 공유 메모리 사용, 포인터 전달
-
디버그 빌드 사용
- 해결: Release 빌드 (-O3, —release)
-
SIMD 미사용
- 해결: SIMD 명령어 활용
성능 측정:
console.time('WASM');
wasmFunction(data);
console.timeEnd('WASM');
console.time('JavaScript');
jsFunction(data);
console.timeEnd('JavaScript');
문제 5: 크로스 오리진 에러 (멀티스레딩)
증상:
SharedArrayBuffer is not defined
해결:
// 서버 응답 헤더 설정 필요
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
# Python Flask 예제
@app.after_request
def add_headers(response):
response.headers['Cross-Origin-Opener-Policy'] = 'same-origin'
response.headers['Cross-Origin-Embedder-Policy'] = 'require-corp'
return response
성능 벤치마크
이미지 처리 (1920x1080 그레이스케일)
| 구현 | 시간 | 상대 성능 |
|---|---|---|
| JavaScript (순수) | 45ms | 1.0x |
| JavaScript (TypedArray 최적화) | 28ms | 1.6x |
| WASM (C++) | 8ms | 5.6x |
| WASM (Rust) | 7ms | 6.4x |
| WASM (Rust + SIMD) | 3ms | 15.0x |
수학 연산 (피보나치 40)
| 구현 | 시간 | 상대 성능 |
|---|---|---|
| JavaScript | 850ms | 1.0x |
| WASM (C++) | 95ms | 8.9x |
| WASM (Rust) | 92ms | 9.2x |
파일 크기 비교
| 구현 | 원본 | 최적화 | Brotli 압축 |
|---|---|---|---|
| C++ (Emscripten) | 450KB | 180KB | 65KB |
| Rust (wasm-pack) | 320KB | 120KB | 42KB |
실전 팁
1. 개발 워크플로우
# 개발 중: 빠른 빌드
wasm-pack build --dev
# 프로덕션: 최적화 빌드
wasm-pack build --release
# 파일 크기 확인
ls -lh pkg/*.wasm
# wasm-opt 추가 최적화
wasm-opt pkg/my_project_bg.wasm -O3 -o pkg/my_project_bg.wasm
2. 디버깅
Chrome DevTools:
- Sources 패널에서 WASM 디버깅 지원
- 중단점 설정 가능
- 변수 검사 가능
console.log 추가:
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
}
pub fn debug_function() {
log(&format!("Debug: value = {}", value));
}
3. 에러 처리
Rust:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn divide(a: f64, b: f64) -> Result<f64, JsValue> {
if b == 0.0 {
Err(JsValue::from_str("Division by zero"))
} else {
Ok(a / b)
}
}
JavaScript:
try {
const result = divide(10, 0);
} catch (error) {
console.error('Error:', error); // "Division by zero"
}
4. 점진적 마이그레이션
단계별 접근:
- 프로파일링: 병목 구간 식별
- 핫 패스 마이그레이션: CPU 집약적 부분만 WASM으로
- 벤치마크: 실제 성능 향상 측정
- 전체 마이그레이션: 필요 시 더 많은 부분 이식
예시:
// 1단계: JavaScript로 전체 구현
function processImage(imageData) {
// 전체 로직
}
// 2단계: 핫 패스만 WASM으로
import { grayscale } from './wasm/image.js';
function processImage(imageData) {
// 전처리 (JavaScript)
// CPU 집약적 부분 (WASM)
grayscale(imageData.data);
// 후처리 (JavaScript)
}
마무리
WebAssembly는 웹에서 네이티브 수준 성능을 실현하는 강력한 도구입니다. 특히:
핵심 포인트:
- C++: 기존 코드베이스 재사용, Emscripten 사용
- Rust: 메모리 안전성, 현대적인 도구 체인 (wasm-pack)
- 성능: JavaScript 대비 3-15배 빠름 (워크로드에 따라)
- 적용 분야: 이미지/비디오 처리, 게임, 암호화, 과학 계산
시작 가이드:
- 간단한 수학 함수부터 시작
- 메모리 관리 패턴 익히기
- 실제 프로젝트에 점진적 적용
- 성능 측정 후 확장
다음 단계:
참고 자료: