본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 5장: Rust에서 Wasm 빌드
2026년 3월 30일·프로그래밍·

5장: Rust에서 Wasm 빌드

Rust에서 WebAssembly를 빌드하는 전체 과정을 다룹니다. wasm-pack, cargo-component, 크기 최적화, WASI 타겟 빌드, 컴포넌트 모델 적용, HTTP 핸들러 실전 예제까지.

13분970.92자10개 섹션
webassemblyrust
공유
webassembly5 / 10
12345678910
이전4장: 컴포넌트 모델과 WIT다음6장: Go, Python, 기타 언어에서 Wasm

학습 목표

  • Rust에서 Wasm을 빌드하기 위한 도구 체인을 설정합니다
  • wasm-pack과 cargo-component의 차이와 사용법을 익힙니다
  • Wasm 바이너리 크기 최적화 기법을 적용합니다
  • Spin 프레임워크로 HTTP 핸들러를 작성합니다

왜 Rust인가

WebAssembly를 타겟으로 컴파일할 수 있는 언어는 다양하지만, Rust가 가장 성숙한 Wasm 생태계를 갖추고 있습니다. 그 이유는 분명합니다.

가비지 컬렉터가 없습니다. Rust는 소유권 시스템으로 메모리를 관리하므로, GC 런타임을 Wasm 바이너리에 포함시킬 필요가 없습니다. 결과적으로 바이너리 크기가 작고 메모리 사용이 예측 가능합니다.

성능이 우수합니다. LLVM 기반 컴파일러가 고도로 최적화된 Wasm 코드를 생성합니다. 네이티브 코드 대비 10~20% 이내의 성능을 달성합니다.

Wasm 생태계의 핵심 도구가 Rust로 작성되어 있습니다. Wasmtime, wasm-tools, cargo-component, Spin 등 대부분의 핵심 도구가 Rust 프로젝트입니다.

도구 체인 설정

기본 설정

setup.sh
bash
# 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-wasip1WASI Preview 1레거시, 단일 모듈
wasm32-wasip2WASI Preview 2컴포넌트 모델, 최신 표준

wasm-pack — 브라우저 Wasm 빌드

wasm-pack은 Rust 코드를 브라우저에서 사용할 수 있는 Wasm + JavaScript 글루 코드로 패키징하는 도구입니다. wasm-bindgen과 긴밀하게 통합됩니다.

src/lib.rs
rust
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()
    }
}
Cargo.toml
toml
[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        # 디버그 심볼 제거
build-wasm-pack.sh
bash
# 빌드 (web 타겟)
wasm-pack build --target web --release
 
# 생성되는 파일:
# pkg/
#   image_processor.js       # JavaScript 글루 코드
#   image_processor_bg.wasm  # Wasm 바이너리
#   image_processor.d.ts     # TypeScript 타입 정의
#   package.json

cargo-component — 컴포넌트 모델 빌드

WASI Preview 2와 컴포넌트 모델을 사용하려면 cargo-component를 사용합니다. wasm-pack과 달리, WIT 기반의 인터페이스를 자동으로 바인딩합니다.

create-component.sh
bash
# 새 컴포넌트 프로젝트 생성
cargo component new greeting-service
cd greeting-service
wit/world.wit
wit
package 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;
}
src/lib.rs
rust
// 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);
build-component.sh
bash
# 컴포넌트 빌드
cargo component build --release
 
# 결과 확인
wasm-tools component wit target/wasm32-wasip2/release/greeting_service.wasm
Info

cargo-component는 빌드 시 WIT 파일을 읽어 Rust 바인딩 코드를 자동 생성합니다. bindings 모듈은 빌드 타임에 생성되므로 소스 코드에는 존재하지 않습니다. IDE에서 타입 오류가 표시될 수 있지만, 빌드에는 문제가 없습니다.

바이너리 크기 최적화

Wasm 바이너리의 크기는 전송 시간과 인스턴스화 속도에 직접적인 영향을 미칩니다. Rust에서 Wasm 크기를 최적화하는 기법을 단계적으로 살펴보겠습니다.

1단계: Cargo 프로필 설정

Cargo.toml
toml
[profile.release]
opt-level = "z"      # 크기 최적화 (s보다 더 공격적)
lto = true           # 링크 타임 최적화
codegen-units = 1    # 전체 프로그램 최적화
panic = "abort"      # panic 시 즉시 종료 (unwind 코드 제거)
strip = true         # 심볼 제거

2단계: wasm-opt 적용

optimize.sh
bash
# binaryen의 wasm-opt으로 추가 최적화
wasm-opt -Oz -o optimized.wasm input.wasm
 
# 크기 비교
ls -lh input.wasm optimized.wasm
# input.wasm:     150KB
# optimized.wasm:  95KB  (약 37% 감소)

3단계: 불필요한 코드 제거

no-std-example.rs
rust
// 표준 라이브러리의 포맷팅 기능만으로도 수십 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" + LTO180 KB-91%
wasm-opt -Oz120 KB-94%
panic=abort + strip95 KB-95%

Spin HTTP 핸들러 실전 예제

이제 배운 내용을 종합하여, Spin 프레임워크로 실제 HTTP API 서버를 작성해 보겠습니다.

create-spin-app.sh
bash
# Spin CLI 설치
curl -fsSL https://developer.fermyon.com/downloads/install.sh | bash
 
# 새 Spin 프로젝트 생성
spin new -t http-rust todo-api
cd todo-api
spin.toml
toml
spin_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"
src/lib.rs
rust
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)
}
run-spin.sh
bash
# 빌드 및 실행
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/todos
Warning

Spin의 Key-Value Store는 기본적으로 인메모리입니다. 프로덕션 환경에서는 SQLite 백엔드나 Redis를 설정해야 데이터가 영속됩니다. runtime-config.toml에서 백엔드를 지정할 수 있습니다.

wasm-tools 활용

wasm-tools는 Wasm 바이너리를 검사하고 조작하는 스위스 군용 칼입니다.

wasm-tools-examples.sh
bash
# 모듈 정보 확인
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를 빌드하는 실전 방법을 다루었습니다.

  • Rust는 GC 없는 설계, 우수한 성능, 풍부한 도구 생태계로 Wasm 개발에 최적화되어 있습니다
  • wasm-pack은 브라우저 Wasm을, cargo-component는 컴포넌트 모델 Wasm을 빌드합니다
  • 프로필 설정, wasm-opt, 코드 패턴 개선으로 바이너리 크기를 95%까지 줄일 수 있습니다
  • Spin 프레임워크로 WASI 기반 HTTP 서버를 빠르게 구축할 수 있습니다

다음 장 미리보기

6장에서는 Rust 외의 언어에서 Wasm을 빌드하는 방법을 살펴봅니다. Go(TinyGo), Python, C/C++, AssemblyScript, .NET 각각의 Wasm 지원 수준과 제약 사항을 비교하고, 프로젝트에 적합한 언어를 선택하는 가이드를 제공합니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#webassembly#rust

관련 글

프로그래밍

6장: Go, Python, 기타 언어에서 Wasm

TinyGo, Python(componentize-py), C/C++(Emscripten), AssemblyScript, .NET Blazor 등 다양한 언어의 Wasm 지원 현황과 제약 사항, 언어 선택 가이드를 다룹니다.

2026년 4월 1일·14분
프로그래밍

4장: 컴포넌트 모델과 WIT

WebAssembly 컴포넌트 모델의 필요성, WIT IDL의 문법과 타입 시스템, 인터페이스와 월드 정의, 컴포넌트 구성을 통한 언어 간 상호운용성을 다룹니다.

2026년 3월 28일·13분
프로그래밍

7장: 브라우저 고성능 앱 — Wasm의 원래 영역

JavaScript와 Wasm의 상호 호출, Web API 연동, 이미지/비디오 처리, AI 추론, 게임 엔진 등 브라우저에서 WebAssembly로 고성능 애플리케이션을 구축하는 방법을 다룹니다.

2026년 4월 3일·14분
이전 글4장: 컴포넌트 모델과 WIT
다음 글6장: Go, Python, 기타 언어에서 Wasm

댓글

목차

약 13분 남음
  • 학습 목표
  • 왜 Rust인가
  • 도구 체인 설정
    • 기본 설정
    • 타겟 선택 가이드
  • wasm-pack — 브라우저 Wasm 빌드
  • cargo-component — 컴포넌트 모델 빌드
  • 바이너리 크기 최적화
    • 1단계: Cargo 프로필 설정
    • 2단계: wasm-opt 적용
    • 3단계: 불필요한 코드 제거
    • 최적화 효과 비교
  • Spin HTTP 핸들러 실전 예제
  • wasm-tools 활용
  • 정리
  • 다음 장 미리보기