JavaScript와 Wasm의 상호 호출, Web API 연동, 이미지/비디오 처리, AI 추론, 게임 엔진 등 브라우저에서 WebAssembly로 고성능 애플리케이션을 구축하는 방법을 다룹니다.
WebAssembly가 JavaScript를 대체하는 기술이라는 오해가 있지만, 실제로는 보완 관계에 가깝습니다. JavaScript는 DOM 조작, 이벤트 처리, UI 로직에서 여전히 최적이며, Wasm은 연산 집약적인 작업에서 진가를 발휘합니다.
브라우저에서 Wasm 모듈을 로드하고 함수를 호출하는 기본 패턴은 다음과 같습니다.
// Wasm 모듈 로드 (스트리밍 컴파일)
const { instance } = await WebAssembly.instantiateStreaming(
fetch('processor.wasm'),
{
env: {
// Wasm에서 호출할 수 있는 JavaScript 함수
consoleLog: (ptr, len) => {
const bytes = new Uint8Array(instance.exports.memory.buffer, ptr, len);
const text = new TextDecoder().decode(bytes);
console.log(text);
}
}
}
);
// Wasm 함수 호출
const result = instance.exports.process(42);이 저수준 API는 정수와 메모리 포인터만 주고받을 수 있습니다. 문자열이나 객체를 전달하려면 직접 메모리를 관리해야 하므로 번거롭습니다.
wasm-bindgen은 Rust와 JavaScript 사이의 고수준 바인딩을 자동 생성하는 도구입니다. 문자열, 객체, 클로저 등을 투명하게 주고받을 수 있습니다.
use wasm_bindgen::prelude::*;
// JavaScript 함수를 Rust에서 호출
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
#[wasm_bindgen(js_namespace = performance)]
fn now() -> f64;
}
// Rust 함수를 JavaScript에서 호출
#[wasm_bindgen]
pub fn parse_markdown(input: &str) -> String {
let start = now();
// Markdown 파싱 (예시)
let result = input
.lines()
.map(|line| {
if line.starts_with("# ") {
format!("<h1>{}</h1>", &line[2..])
} else if line.starts_with("## ") {
format!("<h2>{}</h2>", &line[3..])
} else if line.is_empty() {
String::new()
} else {
format!("<p>{}</p>", line)
}
})
.collect::<Vec<_>>()
.join("\n");
let elapsed = now() - start;
log(&format!("Parsed in {:.2}ms", elapsed));
result
}import init, { parse_markdown } from './pkg/markdown_parser.js';
async function main() {
// Wasm 초기화
await init();
// Rust 함수를 마치 JavaScript 함수처럼 호출
const html = parse_markdown("# Hello\n\nThis is **WebAssembly**.");
document.getElementById('content').innerHTML = html;
}
main();web-sys 크레이트는 모든 Web API에 대한 Rust 바인딩을 제공합니다. DOM 조작, Canvas, Fetch API, WebSocket 등을 Rust에서 직접 사용할 수 있습니다.
use wasm_bindgen::prelude::*;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
#[wasm_bindgen]
pub struct Renderer {
ctx: CanvasRenderingContext2d,
width: u32,
height: u32,
}
#[wasm_bindgen]
impl Renderer {
#[wasm_bindgen(constructor)]
pub fn new(canvas_id: &str) -> Result<Renderer, JsValue> {
let document = web_sys::window()
.unwrap()
.document()
.unwrap();
let canvas = document
.get_element_by_id(canvas_id)
.unwrap()
.dyn_into::<HtmlCanvasElement>()?;
let ctx = canvas
.get_context("2d")?
.unwrap()
.dyn_into::<CanvasRenderingContext2d>()?;
let width = canvas.width();
let height = canvas.height();
Ok(Renderer { ctx, width, height })
}
pub fn draw_mandelbrot(&self, max_iter: u32) {
let image_data = self.ctx
.get_image_data(0.0, 0.0, self.width as f64, self.height as f64)
.unwrap();
let mut data = image_data.data().0;
for y in 0..self.height {
for x in 0..self.width {
// 만델브로 집합 계산
let cx = (x as f64 / self.width as f64) * 3.5 - 2.5;
let cy = (y as f64 / self.height as f64) * 2.0 - 1.0;
let mut zx = 0.0_f64;
let mut zy = 0.0_f64;
let mut iter = 0u32;
while zx * zx + zy * zy < 4.0 && iter < max_iter {
let tmp = zx * zx - zy * zy + cx;
zy = 2.0 * zx * zy + cy;
zx = tmp;
iter += 1;
}
let idx = ((y * self.width + x) * 4) as usize;
if iter == max_iter {
data[idx] = 0;
data[idx + 1] = 0;
data[idx + 2] = 0;
} else {
let t = iter as f64 / max_iter as f64;
data[idx] = (9.0 * (1.0 - t) * t * t * t * 255.0) as u8;
data[idx + 1] = (15.0 * (1.0 - t) * (1.0 - t) * t * t * 255.0) as u8;
data[idx + 2] = (8.5 * (1.0 - t) * (1.0 - t) * (1.0 - t) * t * 255.0) as u8;
}
data[idx + 3] = 255; // alpha
}
}
let clamped = wasm_bindgen::Clamped(data);
let new_image = web_sys::ImageData::new_with_u8_clamped_array_and_sh(
clamped, self.width, self.height
).unwrap();
self.ctx.put_image_data(&new_image, 0.0, 0.0).unwrap();
}
}Canvas 기반 렌더링에서 Wasm은 JavaScript 대비 2~5배 빠른 성능을 보입니다. 특히 픽셀 단위의 반복 연산(프랙탈, 이미지 필터, 시뮬레이션)에서 차이가 두드러집니다. 다만 DOM 조작은 여전히 JavaScript 쪽에서 하는 것이 효율적입니다.
브라우저에서의 실시간 이미지 처리는 Wasm의 대표적인 강점 영역입니다.
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn apply_sepia(pixels: &mut [u8]) {
// RGBA 픽셀 배열을 직접 처리
for chunk in pixels.chunks_exact_mut(4) {
let r = chunk[0] as f32;
let g = chunk[1] as f32;
let b = chunk[2] as f32;
chunk[0] = ((0.393 * r + 0.769 * g + 0.189 * b).min(255.0)) as u8;
chunk[1] = ((0.349 * r + 0.686 * g + 0.168 * b).min(255.0)) as u8;
chunk[2] = ((0.272 * r + 0.534 * g + 0.131 * b).min(255.0)) as u8;
// alpha 유지
}
}
#[wasm_bindgen]
pub fn resize_image(
src: &[u8],
src_width: u32,
src_height: u32,
dst_width: u32,
dst_height: u32,
) -> Vec<u8> {
let mut dst = vec![0u8; (dst_width * dst_height * 4) as usize];
let x_ratio = src_width as f64 / dst_width as f64;
let y_ratio = src_height as f64 / dst_height as f64;
for y in 0..dst_height {
for x in 0..dst_width {
let src_x = (x as f64 * x_ratio) as u32;
let src_y = (y as f64 * y_ratio) as u32;
let src_idx = ((src_y * src_width + src_x) * 4) as usize;
let dst_idx = ((y * dst_width + x) * 4) as usize;
dst[dst_idx..dst_idx + 4].copy_from_slice(&src[src_idx..src_idx + 4]);
}
}
dst
}ONNX Runtime Web은 ONNX 모델을 브라우저에서 실행하는 라이브러리로, 내부적으로 WebAssembly를 사용합니다.
import * as ort from 'onnxruntime-web';
async function classifyImage(imageData) {
// Wasm 백엔드 설정
ort.env.wasm.wasmPaths = '/wasm/';
// 모델 로드
const session = await ort.InferenceSession.create(
'/models/mobilenet-v2.onnx',
{ executionProviders: ['wasm'] }
);
// 입력 텐서 생성
const tensor = new ort.Tensor('float32', imageData, [1, 3, 224, 224]);
// 추론 실행
const results = await session.run({ input: tensor });
const output = results.output.data;
// 결과 해석
const topK = Array.from(output)
.map((prob, idx) => ({ prob, idx }))
.sort((a, b) => b.prob - a.prob)
.slice(0, 5);
return topK;
}WebNN(Web Neural Network API)은 브라우저에서 하드웨어 가속 AI 추론을 가능하게 하는 새로운 표준입니다. 2026년 현재 Chrome에서 실험적으로 지원되며, ONNX Runtime Web과 함께 사용하면 GPU 가속을 활용할 수 있습니다. WebNN이 불가능한 환경에서는 Wasm 백엔드로 자동 폴백됩니다.
Unity와 Unreal Engine은 Wasm을 통한 웹 빌드를 지원합니다. 독립적인 웹 게임 엔진도 Wasm을 핵심 기술로 활용합니다.
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
#[wasm_bindgen]
pub struct Game {
player_x: f64,
player_y: f64,
velocity_x: f64,
velocity_y: f64,
}
#[wasm_bindgen]
impl Game {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
player_x: 400.0,
player_y: 300.0,
velocity_x: 0.0,
velocity_y: 0.0,
}
}
// 매 프레임 호출
pub fn update(&mut self, delta_time: f64) {
self.player_x += self.velocity_x * delta_time;
self.player_y += self.velocity_y * delta_time;
// 경계 충돌 처리
if self.player_x < 0.0 || self.player_x > 800.0 {
self.velocity_x = -self.velocity_x;
}
if self.player_y < 0.0 || self.player_y > 600.0 {
self.velocity_y = -self.velocity_y;
}
}
pub fn set_velocity(&mut self, vx: f64, vy: f64) {
self.velocity_x = vx;
self.velocity_y = vy;
}
pub fn player_x(&self) -> f64 { self.player_x }
pub fn player_y(&self) -> f64 { self.player_y }
}import init, { Game } from './pkg/game_engine.js';
async function main() {
await init();
const game = new Game();
const canvas = document.getElementById('game-canvas');
const ctx = canvas.getContext('2d');
let lastTime = performance.now();
function gameLoop(currentTime) {
const deltaTime = (currentTime - lastTime) / 1000;
lastTime = currentTime;
// Wasm에서 게임 로직 업데이트
game.update(deltaTime);
// JavaScript에서 렌더링
ctx.clearRect(0, 0, 800, 600);
ctx.fillStyle = '#3498db';
ctx.beginPath();
ctx.arc(game.player_x(), game.player_y(), 20, 0, Math.PI * 2);
ctx.fill();
requestAnimationFrame(gameLoop);
}
requestAnimationFrame(gameLoop);
}
main();브라우저에서 Wasm을 배포할 때 바이너리 크기는 사용자 경험에 직접적인 영향을 미칩니다.
// 기본 기능은 JavaScript로
function quickPreview(text) {
return text.substring(0, 200) + '...';
}
// 무거운 처리는 Wasm을 지연 로딩
let wasmModule = null;
async function fullProcess(text) {
if (!wasmModule) {
// 사용자가 실제로 필요할 때만 로드
wasmModule = await import('./pkg/heavy_processor.js');
await wasmModule.default();
}
return wasmModule.process(text);
}// compileStreaming은 다운로드와 컴파일을 병렬로 수행
// 일반 compile 대비 로딩 시간이 크게 단축됨
const module = await WebAssembly.compileStreaming(
fetch('processor.wasm')
);
// 모듈을 캐시하여 재사용
const cache = await caches.open('wasm-cache-v1');
await cache.put('processor', new Response(await fetch('processor.wasm')));WebAssembly.compileStreaming을 사용하려면 서버가 .wasm 파일에 application/wasm MIME 타입을 설정해야 합니다. 잘못된 MIME 타입은 스트리밍 컴파일 실패의 가장 흔한 원인입니다.
Wasm 도입을 결정할 때는 실제 벤치마크가 중요합니다.
| 작업 유형 | JS vs Wasm | 권장 |
|---|---|---|
| DOM 조작 | JS가 빠름 | JavaScript |
| 문자열 처리 (소량) | 비슷함 | JavaScript |
| 숫자 연산 (대량) | Wasm 2~5배 빠름 | WebAssembly |
| 이미지 픽셀 처리 | Wasm 3~10배 빠름 | WebAssembly |
| 암호화/해싱 | Wasm 5~20배 빠름 | WebAssembly |
| JSON 파싱 | 비슷함 ~ JS가 빠름 | JavaScript |
이번 장에서는 브라우저에서의 WebAssembly 활용을 다양한 각도에서 살펴보았습니다.
8장에서는 브라우저를 벗어나 서버사이드 Wasm을 다룹니다. Spin 프레임워크의 아키텍처, Fermyon Cloud와 Akamai의 통합, SpinKube를 활용한 Kubernetes 배포, 그리고 Docker와 Wasm의 비교를 살펴봅니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
Spin 프레임워크의 아키텍처, Fermyon Cloud와 Akamai 통합, SpinKube를 활용한 Kubernetes 배포, Docker와 Wasm의 비교를 통해 서버사이드 WebAssembly의 현재를 분석합니다.
Cloudflare Workers, Fastly Compute, Akamai Edge의 Wasm 실행 환경을 비교하고, 엣지에서의 AI 추론, 콜드 스타트 성능 분석, 엣지 배포 전략을 다룹니다.
Rust+Spin 서버리스 API 구축, 브라우저 Wasm 모듈 통합, 엣지 배포, 성능 벤치마킹, Wasm 도입 의사결정 가이드, 그리고 WebAssembly의 미래 전망을 다룹니다.