Rust에서 WebAssembly를 빌드하는 전체 과정을 다룹니다. wasm-pack, cargo-component, 크기 최적화, WASI 타겟 빌드, 컴포넌트 모델 적용, HTTP 핸들러 실전 예제까지.
WebAssembly를 타겟으로 컴파일할 수 있는 언어는 다양하지만, Rust가 가장 성숙한 Wasm 생태계를 갖추고 있습니다. 그 이유는 분명합니다.
가비지 컬렉터가 없습니다. Rust는 소유권 시스템으로 메모리를 관리하므로, GC 런타임을 Wasm 바이너리에 포함시킬 필요가 없습니다. 결과적으로 바이너리 크기가 작고 메모리 사용이 예측 가능합니다.
성능이 우수합니다. LLVM 기반 컴파일러가 고도로 최적화된 Wasm 코드를 생성합니다. 네이티브 코드 대비 10~20% 이내의 성능을 달성합니다.
Wasm 생태계의 핵심 도구가 Rust로 작성되어 있습니다. Wasmtime, wasm-tools, cargo-component, Spin 등 대부분의 핵심 도구가 Rust 프로젝트입니다.
# Rust 설치 (이미 설치되어 있다면 생략)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Wasm 타겟 추가
rustup target add wasm32-unknown-unknown # 브라우저용
rustup target add wasm32-wasip1 # WASI Preview 1
rustup target add wasm32-wasip2 # WASI Preview 2
# 핵심 도구 설치
cargo install wasm-pack # 브라우저 Wasm 패키징
cargo install cargo-component # 컴포넌트 모델 빌드
cargo install wasm-tools # Wasm 바이너리 조작
cargo install wasm-opt # binaryen 최적화| 타겟 | 용도 | 특징 |
|---|---|---|
wasm32-unknown-unknown | 브라우저, 범용 | WASI 없음, 최소 환경 |
wasm32-wasip1 | WASI Preview 1 | 레거시, 단일 모듈 |
wasm32-wasip2 | WASI Preview 2 | 컴포넌트 모델, 최신 표준 |
wasm-pack은 Rust 코드를 브라우저에서 사용할 수 있는 Wasm + JavaScript 글루 코드로 패키징하는 도구입니다. wasm-bindgen과 긴밀하게 통합됩니다.
use wasm_bindgen::prelude::*;
// JavaScript에서 호출할 수 있는 함수
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u64 {
match n {
0 => 0,
1 => 1,
_ => {
let mut a: u64 = 0;
let mut b: u64 = 1;
for _ in 2..=n {
let temp = a + b;
a = b;
b = temp;
}
b
}
}
}
// JavaScript의 구조체를 Rust에서 사용
#[wasm_bindgen]
pub struct ImageProcessor {
width: u32,
height: u32,
pixels: Vec<u8>,
}
#[wasm_bindgen]
impl ImageProcessor {
#[wasm_bindgen(constructor)]
pub fn new(width: u32, height: u32) -> Self {
Self {
width,
height,
pixels: vec![0; (width * height * 4) as usize],
}
}
pub fn grayscale(&mut self) {
for chunk in self.pixels.chunks_exact_mut(4) {
let r = chunk[0] as f32;
let g = chunk[1] as f32;
let b = chunk[2] as f32;
let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
chunk[0] = gray;
chunk[1] = gray;
chunk[2] = gray;
// alpha 채널은 유지
}
}
pub fn pixels_ptr(&self) -> *const u8 {
self.pixels.as_ptr()
}
pub fn pixels_len(&self) -> usize {
self.pixels.len()
}
}[package]
name = "image-processor"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
[profile.release]
opt-level = "z" # 크기 최적화
lto = true # 링크 타임 최적화
codegen-units = 1 # 단일 코드 생성 유닛
strip = true # 디버그 심볼 제거# 빌드 (web 타겟)
wasm-pack build --target web --release
# 생성되는 파일:
# pkg/
# image_processor.js # JavaScript 글루 코드
# image_processor_bg.wasm # Wasm 바이너리
# image_processor.d.ts # TypeScript 타입 정의
# package.jsonWASI Preview 2와 컴포넌트 모델을 사용하려면 cargo-component를 사용합니다. wasm-pack과 달리, WIT 기반의 인터페이스를 자동으로 바인딩합니다.
# 새 컴포넌트 프로젝트 생성
cargo component new greeting-service
cd greeting-servicepackage example:greeting@1.0.0;
interface greet {
record greeting-input {
name: string,
language: string,
}
greet: func(input: greeting-input) -> string;
}
world greeting-service {
export greet;
}// cargo-component가 생성한 바인딩을 사용
mod bindings;
use bindings::exports::example::greeting::greet::{Guest, GreetingInput};
struct Component;
impl Guest for Component {
fn greet(input: GreetingInput) -> String {
match input.language.as_str() {
"ko" => format!("안녕하세요, {}님!", input.name),
"ja" => format!("こんにちは、{}さん!", input.name),
"en" | _ => format!("Hello, {}!", input.name),
}
}
}
bindings::export!(Component with_types_in bindings);# 컴포넌트 빌드
cargo component build --release
# 결과 확인
wasm-tools component wit target/wasm32-wasip2/release/greeting_service.wasmcargo-component는 빌드 시 WIT 파일을 읽어 Rust 바인딩 코드를 자동 생성합니다. bindings 모듈은 빌드 타임에 생성되므로 소스 코드에는 존재하지 않습니다. IDE에서 타입 오류가 표시될 수 있지만, 빌드에는 문제가 없습니다.
Wasm 바이너리의 크기는 전송 시간과 인스턴스화 속도에 직접적인 영향을 미칩니다. Rust에서 Wasm 크기를 최적화하는 기법을 단계적으로 살펴보겠습니다.
[profile.release]
opt-level = "z" # 크기 최적화 (s보다 더 공격적)
lto = true # 링크 타임 최적화
codegen-units = 1 # 전체 프로그램 최적화
panic = "abort" # panic 시 즉시 종료 (unwind 코드 제거)
strip = true # 심볼 제거# binaryen의 wasm-opt으로 추가 최적화
wasm-opt -Oz -o optimized.wasm input.wasm
# 크기 비교
ls -lh input.wasm optimized.wasm
# input.wasm: 150KB
# optimized.wasm: 95KB (약 37% 감소)// 표준 라이브러리의 포맷팅 기능만으로도 수십 KB가 추가됩니다
// 불필요한 경우 제거를 고려하세요
// 이렇게 하면 크기가 큽니다
fn process(input: &str) -> String {
format!("Result: {}", input) // fmt 기계 전체가 포함됨
}
// 이렇게 하면 작습니다
fn process(input: &str) -> String {
let mut result = String::from("Result: ");
result.push_str(input);
result
}| 단계 | 크기 | 감소율 |
|---|---|---|
| 기본 빌드 (debug) | 2.1 MB | - |
| release 빌드 | 450 KB | -79% |
| opt-level="z" + LTO | 180 KB | -91% |
| wasm-opt -Oz | 120 KB | -94% |
| panic=abort + strip | 95 KB | -95% |
이제 배운 내용을 종합하여, Spin 프레임워크로 실제 HTTP API 서버를 작성해 보겠습니다.
# Spin CLI 설치
curl -fsSL https://developer.fermyon.com/downloads/install.sh | bash
# 새 Spin 프로젝트 생성
spin new -t http-rust todo-api
cd todo-apispin_manifest_version = 2
[application]
name = "todo-api"
version = "0.1.0"
[[trigger.http]]
route = "/api/todos/..."
component = "todo-api"
[component.todo-api]
source = "target/wasm32-wasip2/release/todo_api.wasm"
allowed_outbound_hosts = []
key_value_stores = ["default"]
[component.todo-api.build]
command = "cargo component build --release"use spin_sdk::http::{IntoResponse, Request, Response, Method};
use spin_sdk::http_component;
use spin_sdk::key_value::Store;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone)]
struct Todo {
id: String,
title: String,
completed: bool,
}
#[http_component]
fn handle_request(req: Request) -> anyhow::Result<impl IntoResponse> {
let path = req.path();
let method = req.method();
match (method, path) {
// 전체 목록 조회
(Method::Get, "/api/todos") => list_todos(),
// 단일 조회
(Method::Get, p) if p.starts_with("/api/todos/") => {
let id = p.strip_prefix("/api/todos/").unwrap_or("");
get_todo(id)
}
// 생성
(Method::Post, "/api/todos") => {
let body = req.body();
create_todo(body)
}
// 그 외
_ => Ok(Response::builder()
.status(404)
.header("content-type", "application/json")
.body(r#"{"error":"Not Found"}"#)
.build()),
}
}
fn list_todos() -> anyhow::Result<Response> {
let store = Store::open_default()?;
let keys = store.get_keys()?;
let mut todos: Vec<Todo> = Vec::new();
for key in keys {
if let Some(value) = store.get(&key)? {
if let Ok(todo) = serde_json::from_slice::<Todo>(&value) {
todos.push(todo);
}
}
}
let body = serde_json::to_string(&todos)?;
Ok(Response::builder()
.status(200)
.header("content-type", "application/json")
.body(body)
.build())
}
fn get_todo(id: &str) -> anyhow::Result<Response> {
let store = Store::open_default()?;
match store.get(id)? {
Some(value) => Ok(Response::builder()
.status(200)
.header("content-type", "application/json")
.body(value)
.build()),
None => Ok(Response::builder()
.status(404)
.header("content-type", "application/json")
.body(r#"{"error":"Todo not found"}"#)
.build()),
}
}
fn create_todo(body: &[u8]) -> anyhow::Result<Response> {
let mut todo: Todo = serde_json::from_slice(body)?;
todo.id = uuid_v4();
let store = Store::open_default()?;
let value = serde_json::to_vec(&todo)?;
store.set(&todo.id, &value)?;
let body = serde_json::to_string(&todo)?;
Ok(Response::builder()
.status(201)
.header("content-type", "application/json")
.body(body)
.build())
}
fn uuid_v4() -> String {
// 간단한 UUID 생성 (실제로는 uuid 크레이트 사용 권장)
use spin_sdk::variables;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
format!("{:032x}", timestamp)
}# 빌드 및 실행
spin build
spin up
# 테스트
curl -X POST http://localhost:3000/api/todos \
-H "Content-Type: application/json" \
-d '{"title":"Learn WebAssembly","completed":false}'
curl http://localhost:3000/api/todosSpin의 Key-Value Store는 기본적으로 인메모리입니다. 프로덕션 환경에서는 SQLite 백엔드나 Redis를 설정해야 데이터가 영속됩니다. runtime-config.toml에서 백엔드를 지정할 수 있습니다.
wasm-tools는 Wasm 바이너리를 검사하고 조작하는 스위스 군용 칼입니다.
# 모듈 정보 확인
wasm-tools print app.wasm | head -20
# 컴포넌트의 WIT 인터페이스 추출
wasm-tools component wit app.wasm
# 코어 모듈을 컴포넌트로 변환
wasm-tools component new core.wasm -o component.wasm
# 컴포넌트 유효성 검증
wasm-tools validate --features component-model app.wasm
# 바이너리 크기 분석
wasm-tools strip app.wasm -o stripped.wasm이번 장에서는 Rust에서 WebAssembly를 빌드하는 실전 방법을 다루었습니다.
6장에서는 Rust 외의 언어에서 Wasm을 빌드하는 방법을 살펴봅니다. Go(TinyGo), Python, C/C++, AssemblyScript, .NET 각각의 Wasm 지원 수준과 제약 사항을 비교하고, 프로젝트에 적합한 언어를 선택하는 가이드를 제공합니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
TinyGo, Python(componentize-py), C/C++(Emscripten), AssemblyScript, .NET Blazor 등 다양한 언어의 Wasm 지원 현황과 제약 사항, 언어 선택 가이드를 다룹니다.
WebAssembly 컴포넌트 모델의 필요성, WIT IDL의 문법과 타입 시스템, 인터페이스와 월드 정의, 컴포넌트 구성을 통한 언어 간 상호운용성을 다룹니다.
JavaScript와 Wasm의 상호 호출, Web API 연동, 이미지/비디오 처리, AI 추론, 게임 엔진 등 브라우저에서 WebAssembly로 고성능 애플리케이션을 구축하는 방법을 다룹니다.