Skip to content
Published on

WebAssembly Deep Dive — Wasm VM, Wasmtime, WASI, Component Model, Edge Computing 완전 정복 (2025)

Authors

TL;DR

  • WebAssembly는 스택 기반 가상 머신의 이식 가능한 바이너리 포맷이다. 검증 가능(verifiable), 샌드박스, 즉시 실행 가능이 3대 목표.
  • 구조화된 제어 흐름: goto/jmp 없음. block/loop/if/br만. 덕분에 단일 패스 검증 + 스트리밍 컴파일 가능.
  • 선형 메모리: 각 모듈은 바이트 배열 하나를 가진다. 포인터는 그 배열의 오프셋. 호스트 메모리는 절대 접근 불가.
  • 런타임 구현: Wasmtime(Cranelift JIT), V8(TurboFan + Liftoff), Wasmer, WasmEdge, Wasmi(인터프리터) 등. 각자 다른 최적화 전략.
  • WASI: POSIX 유사 시스템 인터페이스. 파일/네트워크/시계 접근을 "capability 기반"으로 제공.
  • Component Model (2024+): Wasm의 가장 큰 한계(언어 간 타입 없는 ABI)를 해결. WIT IDL로 인터페이스 정의, Canonical ABI로 구현.
  • 실전 활용: Cloudflare Workers, Fastly Compute@Edge, Shopify Functions, Envoy WASM filters, Istio, Docker+Wasm, Kubernetes runtime class, Plugin 시스템.
  • 한계: GC 제안은 아직 진화 중, SIMD/멀티스레드는 부분 지원, Wasm을 타겟으로 컴파일되는 언어는 여전히 제한적(Rust/C/C++/Go/AssemblyScript/Zig/Swift 등).

1. Wasm은 왜 생겼는가

1.1 asm.js의 유산

2013년, Mozilla의 Alon Zakai가 asm.js를 공개했다. 목표: C/C++로 작성된 게임을 브라우저에서 네이티브에 가까운 속도로 돌리는 것. 기법: JavaScript의 작은 서브셋을 써서 JIT이 "이건 정수 덧셈이고 이건 배열 접근이다"를 바로 알 수 있게.

function add(a, b) {
  a = a|0;  // a는 int
  b = b|0;  // b는 int
  return (a + b)|0;
}

|0 연산자가 힌트다. V8과 SpiderMonkey가 이를 인식하고 특별 경로로 처리했다. 결과: C 대비 50% 속도. 하지만 여전히 JavaScript 파서를 거쳐야 했고, 바이너리가 크고 파싱이 느렸다.

1.2 모두가 동의한 순간

2015년, Google, Mozilla, Microsoft, Apple이 드물게 합의했다: "asm.js를 바이너리로 만들자." 이것이 WebAssembly의 시작이다.

목표:

  1. 빠름: 디코딩과 검증을 단일 패스로.
  2. 안전: 검증된 코드는 메모리 안전.
  3. 이식 가능: CPU 아키텍처 독립.
  4. 언어 독립: JavaScript만이 아니라 C/C++, Rust, Go, Swift 등 무엇이든 컴파일 가능.

1.3 브라우저 밖으로

2017년 MVP(Minimum Viable Product)가 4대 브라우저에 출시됐다. 그런데 예상하지 못한 일이 벌어졌다. Wasm의 이점(샌드박스, 이식성, 빠른 시작)이 브라우저 밖에서도 매력적이었다.

  • Cloudflare Workers (2018): V8의 isolate를 재활용해 Wasm을 서버리스로.
  • Fastly Compute@Edge (2019): Wasmtime을 엣지 네트워크에서 사용.
  • Istio / Envoy (2019): 사이드카에 Wasm 필터로 커스텀 로직 주입.
  • Shopify Functions (2022): 고객이 Wasm으로 체크아웃 로직 확장.
  • Docker Desktop (2023): docker run --runtime=io.containerd.wasmedge.v1.
  • Kubernetes (2023): RuntimeClass로 Wasm 워크로드 스케줄링.

2025년 현재, Wasm은 단순히 "브라우저 기술"이 아니다. 경량 샌드박스가 필요한 모든 곳 — 엣지 컴퓨팅, 플러그인 시스템, 서버리스, 다국어 프로그램 호환성 — 에 쓰인다.


2. Wasm 바이너리 포맷

2.1 섹션 기반 구조

.wasm 파일은 바이너리 섹션의 시퀀스다:

매직 넘버: 0x00 0x61 0x73 0x6D  ("\0asm")
버전:      0x01 0x00 0x00 0x00  (version 1)

섹션들:
  0: Custom (이름, 디버그 정보 등)
  1: Type      (함수 시그니처)
  2: Import    (import 선언)
  3: Function  (함수 인덱스 → 타입)
  4: Table     (간접 호출 테이블)
  5: Memory    (선형 메모리)
  6: Global    (전역 변수)
  7: Export    (외부 노출)
  8: Start     (자동 실행 함수)
  9: Element   (테이블 초기화)
 10: Code      (함수 바디)
 11: Data      (메모리 초기화)
 12: DataCount (Data 섹션 개수, GC 없이 참조용)

각 섹션은 LEB128 가변 길이 정수로 길이를 명시 → 스트리밍 가능.

2.2 예제: add(a, b) { return a + b; }

C 코드:

int add(int a, int b) { return a + b; }

clang --target=wasm32 컴파일 후 wasm2wat로 Text 포맷:

(module
  (type (;0;) (func (param i32 i32) (result i32)))
  (func $add (type 0) (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add)
  (export "add" (func $add)))

명령어 해석:

  • local.get 0: 첫 번째 파라미터(a)를 스택 push.
  • local.get 1: 두 번째 파라미터(b)를 스택 push.
  • i32.add: 두 값을 pop하여 합을 push.
  • 함수 끝: 스택의 값이 반환.

스택 머신이다. JVM/CLR/Python VM과 유사한 설계. 레지스터 할당을 바이너리가 아닌 JIT이 담당.

2.3 왜 스택 머신인가

대안은 레지스터 머신(LLVM IR, Dalvik)이지만 Wasm은 스택을 택했다. 이유:

  1. 코드 크기: 스택 머신은 오퍼랜드를 명시할 필요 없음 → 바이너리가 작다.
  2. 단순한 검증: 각 명령어의 스택 효과를 따라가며 타입 체크.
  3. JIT 친화적: 스택 머신을 SSA IR로 변환하는 건 linear-time.

단점: 실행 시 네이티브 스택과 맞지 않아 JIT이 항상 필요 (인터프리터는 느림).

2.4 가변 길이 정수: LEB128

Wasm의 모든 정수는 LEB128 압축 인코딩을 쓴다:

00x00              (1 byte)
1270x7F              (1 byte)
1280x80 0x01         (2 bytes)
163840x80 0x80 0x01    (3 bytes)

각 바이트의 최상위 비트가 "더 있음"을 표시, 나머지 7비트가 값. 작은 값은 1바이트, 큰 값은 더. 파일 크기 절약 + 호환성 확장 가능.


3. 실행 모델

3.1 모듈 / 인스턴스 / 스토어

Module: 코드와 메타데이터 (immutable, 공유 가능)
Instance: 모듈의 실행 중 상태 (mutable)
Store: 모든 Instance를 담는 환경

한 모듈로 여러 인스턴스를 만들 수 있다. 각 인스턴스는:

  • 자기 선형 메모리
  • 자기 전역 변수
  • 자기 테이블
// Wasmtime 예시
let engine = Engine::default();
let module = Module::from_file(&engine, "math.wasm")?;

// 같은 모듈로 두 인스턴스
let mut store1 = Store::new(&engine, ());
let inst1 = Instance::new(&mut store1, &module, &[])?;

let mut store2 = Store::new(&engine, ());
let inst2 = Instance::new(&mut store2, &module, &[])?;

3.2 선형 메모리

Wasm 인스턴스는 Memory 오브젝트를 가진다. 이것은 하나의 큰 바이트 배열.

(memory (export "memory") 1)  ;; 초기 1 페이지 (64 KB)

중요한 제약:

  • 메모리는 페이지 단위 (1 page = 64 KB).
  • 초기 크기와 최대 크기 지정 가능.
  • 모든 포인터는 이 배열의 오프셋 (즉, u32).
  • 호스트 메모리 절대 접근 불가.
  • memory.grow N 명령어로 N 페이지 확장.
;; 메모리의 offset 16에 값 42를 저장
i32.const 16
i32.const 42
i32.store

호스트는 이 메모리에 직접 접근할 수 있다 (바이트 배열을 보면 되니까). 그래서 Wasm ↔ 호스트 통신은 주로 "메모리 오프셋"을 주고받는 방식이다.

3.3 샌드박스의 본질

Wasm이 안전한 이유:

  1. 메모리 격리: 선형 메모리 외부에 접근할 방법이 아예 없다. i32.load offset은 메모리 길이 체크 후 접근.
  2. 타입 안전: 모든 함수 호출은 시그니처 일치해야. verifier가 검증.
  3. 제어 흐름 격리: call_indirect는 테이블 범위 + 타입 일치 체크.
  4. 시스템콜 없음: Wasm 자체는 OS를 모른다. WASI가 선택적으로 제공.

이것이 "JS보다 빠르고 네이티브보다 안전한" 의미다.

3.4 구조화된 제어 흐름

Wasm은 goto/jmp 없다. 제어 흐름 연산자는 딱 네 개:

(block $b ... br $b ...)   ;; 전방 점프 (break)
(loop $l ... br $l ...)    ;; 후방 점프 (continue)
(if ... else ... end)      ;; 조건 분기
(br_table ...)             ;; switch/jump table

임의의 위치로 점프 불가. 이로 인해:

  • 검증 단일 패스: 스택 깊이를 블록 경계에서 확인 가능.
  • SSA 변환 쉬움: 블록 구조가 명확 → dominance tree 구축 단순.
  • 스트리밍 컴파일: 함수 단위로 컴파일하면 다음 함수 전에 시작 가능.

이 설계는 교수 Ben Titzer(Google, Wasm 공동 저자)의 아이디어로, Wasm의 가장 중요한 결정 중 하나로 꼽힌다.


4. 검증기

4.1 무엇을 검증하는가

Wasm 모듈이 로드 타임에 검증되어 유효하다고 판명되면, 런타임 메모리 안전성이 보장된다. 검증은:

  1. 시그니처 체크: 각 함수 호출이 선언된 타입과 일치.
  2. 스택 타입 추적: 각 명령어 후 스택의 타입이 올바른가.
  3. 로컬 변수 사용: 로컬의 타입 일치.
  4. 메모리 경계: 메모리 연산의 정적 체크 (offset 포함).
  5. 테이블 경계: call_indirect 테이블 인덱스.

4.2 단일 패스 알고리즘

Wasm은 bytes-to-bytes 하나의 패스로 검증할 수 있도록 설계됐다.

for each 명령어:
    인스트럭션 opcode 확인
    필요한 스택 타입 체크 (pop)
    결과 스택 push
    분기라면 label 스택 관리
end

검증 통과 = 스택이 정확히 함수의 리턴 타입과 일치

단일 패스라는 것은 O(n), 파일 크기에 비례. MB급 Wasm도 수 ms에 검증 완료.

4.3 비교: eBPF vs Wasm 검증

두 기술 모두 바이트코드 검증기를 가진다.

항목WasmeBPF
복잡도O(n) 단일 패스경로별 symbolic execution
루프구조화됨 (br 만)bounded loops
포인터선형 메모리 오프셋타입 있는 포인터
최대 프로그램기본 없음 (구현 한계)~1M 명령어
호환 타겟브라우저, 서버, 엣지Linux 커널

Wasm의 구조화된 제어 흐름이 검증을 단순화하는 반면, eBPF는 일반적인 제어 흐름을 허용하는 대신 verifier가 훨씬 복잡해진다.

4.4 검증 실패 사례

(func (result i32)
  i32.const 1
  i32.const 2
  ;; 스택: [1, 2]
  drop
  drop
  ;; 스택: []
  ;; 함수는 i32를 리턴해야 하는데 스택 비어있음
  )

검증 에러: "type mismatch: expected i32 but stack is empty."

모든 모듈은 로드되기 전에 검증된다. 실패 시 Instantiate 단계에서 예외 발생.


5. JIT 컴파일 전략

5.1 컴파일 전략 비교

전략시작 시간실행 속도구현
인터프리터즉시10-50x 느림wasmi, Wizard
Baseline JIT빠름 (수 ms)1.5-2x 느림V8 Liftoff, Wasmtime single-pass
Optimizing JIT느림 (100ms+)네이티브 - 10%Cranelift, TurboFan
AOT빌드 시간네이티브 - 5%Wasmtime precompile

5.2 계층적 컴파일 (Tiered)

V8과 Wasmtime은 계층적 컴파일을 쓴다:

모듈 로드 → Liftoff (baseline JIT, 10ms)
              → 즉시 실행 가능
              → 백그라운드에서 TurboFan/Cranelift 최적화
              → 함수별로 최적화 완료 시 교체

이 설계의 미덕:

  • 빠른 시작: 첫 실행까지 거의 0ms.
  • 피크 성능: 시간이 지나면 최적화된 코드로 교체.
  • 워크로드 적응: 자주 호출되는 함수만 우선 최적화.

5.3 Cranelift 해부

Wasmtime의 기본 JIT인 Cranelift는 LLVM의 대안으로 설계됐다.

LLVM 대비 특징:

  • 훨씬 빠른 컴파일: LLVM의 100ms → Cranelift의 10ms.
  • 안전성 우선: Rust로 작성, 메모리 안전.
  • 모듈화된 백엔드: x86_64, ARM64, RISC-V, s390x.
  • 제한된 최적화: LLVM만큼의 피크 성능은 못 냄(하지만 근접).

Cranelift의 중간 표현(CLIF):

function u0:0(i32 v0, i32 v1) -> i32 system_v {
block0(v0: i32, v1: i32):
    v2 = iadd v0, v1
    return v2
}

LLVM IR과 유사하지만 SSA만 지원 (GOT, 가변 인자 등 일부 기능 없음).

5.4 Liftoff — Baseline JIT 혁명

V8의 Liftoff는 "가장 단순하고 빠른 JIT"을 목표로 했다.

알고리즘 개요:

  1. Wasm 명령어를 순차 읽기.
  2. 각 명령어를 직접 머신 코드로 변환 (IR 없음!).
  3. 레지스터 할당은 스택 시뮬레이션: Wasm 스택의 top-of-stack을 레지스터에 매핑.
  4. 인라인 캐시 없음, 최적화 없음.

결과: 1-2 GB/s 컴파일 속도. 10 MB Wasm 모듈을 5 ms에 컴파일. TurboFan이 같은 걸 하려면 100 ms가 걸린다.

5.5 SIMD 명령어

Wasm은 128-bit SIMD 명령어를 지원한다 (proposal 2021 stable).

v128.load (memory)         ;; 128-bit 로드
i32x4.add                  ;; 4개의 32-bit 정수 동시 덧셈
f32x4.mul                  ;; 4개의 32-bit 실수 동시 곱셈

x86_64의 SSE, ARM의 NEON으로 컴파일. 이미지 처리, 비디오 인코딩, 암호화 같은 워크로드에서 2-4x 성능 향상.

5.6 멀티스레드

멀티스레드는 thread proposal로 조금씩 들어오는 중:

  • SharedArrayBuffer: 여러 Wasm 인스턴스가 같은 메모리 공유.
  • atomic. 명령어*: atomic.add, atomic.cmpxchg 등.
  • futex 기반 wait/notify: 대기/깨우기.

2025년 현재 브라우저(V8/SpiderMonkey)와 Wasmtime에서 지원. 하지만 여전히 "post-MVP"로 분류되며 Component Model과 상호작용이 복잡.


6. WASI — Wasm의 OS 인터페이스

6.1 필요성

Wasm 바이트코드 자체는 순수다 — OS 개념이 없다. 파일? 네트워크? 환경 변수? 아무것도 없음.

그러면 실용적이지 않다. 해결: WASI (WebAssembly System Interface).

6.2 Capability-Based Security

POSIX 같은 전통 OS는 "프로그램이 시스템콜을 호출하면 권한 체크" 모델. WASI는 다르다.

// Wasmtime 호스트 측 코드
let mut wasi = WasiCtxBuilder::new()
    .inherit_stdio()
    .preopened_dir(Dir::open_ambient("/data", cap_std::ambient_authority())?, "/")
    .build();
  • preopened_dir: "이 프로그램은 /data 디렉토리만 접근 가능".
  • Wasm 코드는 /data의 파일만 open할 수 있다. 다른 디렉토리를 시도하면 권한 에러.
  • 네트워크, 환경 변수 등도 같은 원리.

이것이 capability 기반 보안이다. "권한을 줘야 쓸 수 있다"는 원칙.

6.3 WASI Preview 1 vs Preview 2

Preview 1 (2019):

  • POSIX 유사 API (file, clock, random, env).
  • C ABI 기반.
  • 모든 주요 언어가 컴파일 가능.
  • 한계: 네트워크 없음, 비동기 없음.

Preview 2 (2023+):

  • Component Model 기반.
  • Worlds: 프로그램이 필요한 능력 선언.
  • wasi:http, wasi:filesystem, wasi:sockets 등 모듈화.
  • 비동기 I/O, HTTP 서버 등 최신 패턴.

6.4 예제: WASI로 파일 읽기

Rust:

use std::fs;

fn main() {
    let content = fs::read_to_string("hello.txt").unwrap();
    println!("{}", content);
}
cargo build --target wasm32-wasi
wasmtime --dir=. target/wasm32-wasi/debug/my_program.wasm

--dir=.이 "현재 디렉토리에 접근 허용"이다. Wasm 코드는 이걸 root로 보는 chroot-like 환경. 밖으로 탈출 불가.

6.5 네트워크

WASI Preview 2의 wasi:sockets:

use wasi::sockets::tcp;

let sock = tcp::create_tcp_socket(IpAddressFamily::Ipv4).unwrap();
let addr = IpSocketAddress::Ipv4(Ipv4SocketAddress {
    port: 8080,
    address: (127, 0, 0, 1),
});
sock.bind(&network, addr).unwrap();
sock.listen().unwrap();

호스트가 "이 프로그램은 localhost:8080만 bind 가능"이라고 capability를 제한할 수 있다.


7. Component Model — Wasm의 두 번째 혁명

7.1 해결하려는 문제

초기 Wasm은 C ABI 수준의 언어 상호운용만 가능했다.

[Rust Wasm 모듈] ←→ [Go Wasm 모듈]
  문제: 타입 있는 데이터를 주고받으려면?
  - 포인터를 넘겨줘야 하는데 메모리가 다름
  - 문자열? 구조체? 배열?

해결책들:

  1. 무시: 두 모듈을 별개로 취급. 호스트가 중재.
  2. 공유 메모리: SharedArrayBuffer — 양쪽이 같은 레이아웃 동의해야.
  3. Component Model: 공식 해결책.

7.2 WIT — WebAssembly Interface Types

WIT (WebAssembly Interface Types)는 언어 독립 IDL이다.

package example:greeting;

interface greeter {
    /// A person with a name and age.
    record person {
        name: string,
        age: u32,
    }

    /// Greet a person, returning the message.
    greet: func(p: person) -> string;
}

world my-world {
    export greeter;
}
  • 타입: record, variant, list, tuple, option, result.
  • 함수: func.
  • 인터페이스: 관련 타입과 함수의 그룹.
  • World: 컴포넌트가 import/export하는 전체 인터페이스 집합.

7.3 Canonical ABI

WIT 타입을 Wasm 바이트로 직렬화하는 공식 규칙이 Canonical ABI다.

string "hello"  guest 메모리에 UTF-8 바이트 쓰기
  (offset, length) 쌍을 호출자에게 전달

record { name: "Alice", age: 30 }  name: (offset, length)
  age: i32

각 언어 컴파일러(Rust, Go, JavaScript 등)가 Canonical ABI를 구현하면, 같은 WIT를 공유하는 모든 언어 간 호환.

7.4 Component vs Module

  • Module (기존 Wasm): 저수준 바이트. 함수 import/export만.
  • Component (Component Model): WIT 기반 고수준 인터페이스. 타입 있는 데이터.

Component는 실제로는 Module을 감싼 얇은 wrapper다:

Component:
  ├─ Core Module (Wasm 바이트코드)
  ├─ Canonical Function Definitions (ABI 어댑터)
  └─ Type Information (WIT)

7.5 wit-bindgen

각 언어가 Component를 쓰려면 WIT를 보고 언어 코드를 생성해야 한다.

Rust:

cargo install wit-bindgen-cli
wit-bindgen rust greeter.wit

생성된 trait:

pub trait Greeter {
    fn greet(p: Person) -> String;
}

사용자는 이 trait을 구현하기만 하면 된다:

struct MyGreeter;
impl Greeter for MyGreeter {
    fn greet(p: Person) -> String {
        format!("Hello, {}! You are {} years old.", p.name, p.age)
    }
}

export!(MyGreeter);

다른 언어(Go, JavaScript)도 유사한 바인딩 제공. 한 번 WIT를 쓰면 모든 언어가 호환된다.

7.6 구성(Composition)

Component Model의 힘은 linking이다. 여러 Component를 조합해 새 Component를 만들 수 있다.

wasm-tools compose \
    --definitions web.wit \
    --dependencies logger.wasm,auth.wasm \
    main.wasm \
    -o final.wasm

main.wasmloggerauth를 import하면, compose 결과는 그들을 main 내부에 embedding한 하나의 Component다.

이것이 "Wasm의 npm" 또는 "Wasm의 컨테이너"로 불린다. 언어 독립 라이브러리 생태계의 기반.


8. 런타임 비교

8.1 Wasmtime

  • 회사: Bytecode Alliance (Mozilla, Fastly, Intel 등).
  • 언어: Rust.
  • JIT: Cranelift (ahead-of-time 가능).
  • WASI: Preview 1 & 2 지원.
  • 특징: Component Model의 참조 구현. 프로덕션 사용 많음(Fastly Compute@Edge).
use wasmtime::*;

let engine = Engine::default();
let module = Module::from_file(&engine, "add.wasm")?;
let mut store = Store::new(&engine, ());
let instance = Instance::new(&mut store, &module, &[])?;
let add = instance.get_typed_func::<(i32, i32), i32>(&mut store, "add")?;
let result = add.call(&mut store, (2, 3))?;

8.2 Wasmer

  • 회사: Wasmer Inc.
  • 언어: Rust.
  • JIT: Cranelift, LLVM, singlepass (옵션).
  • 특징: Wasmer Edge (서버리스 플랫폼), WAPM (Wasm 패키지 매니저).

8.3 V8 / SpiderMonkey

  • 브라우저: Chrome/Edge (V8), Firefox (SpiderMonkey), Safari (JavaScriptCore).
  • JIT: V8 = Liftoff + TurboFan. SpiderMonkey = Baseline + Ion.
  • 특징: 가장 많이 쓰이는 구현. 자바스크립트와 긴밀 통합.

8.4 WasmEdge

  • 회사: CNCF (Cloud Native Computing Foundation).
  • 언어: C++.
  • 특징: 엣지 컴퓨팅 특화. AOT, 경량, Docker 통합. Kubernetes RuntimeClass 지원.

8.5 Wasmi

  • 특징: 순수 인터프리터. no_std, 임베디드용. 매우 작음.
  • 속도: JIT보다 10-50x 느림.
  • 용도: 블록체인(Polkadot), 임베디드 디바이스.

8.6 성능 비교 (간단)

런타임시작 시간피크 성능메모리
Wasmtime (Cranelift)~10 ms네이티브 - 5%중간
V8 (Liftoff → TurboFan)~5 ms네이티브 - 5%
Wasmer (LLVM)~100 ms네이티브 - 1%중간
Wasmer (singlepass)~5 ms네이티브 - 30%작음
Wasmi (interpreter)0 ms네이티브 / 10작음

9. 엣지 컴퓨팅 케이스 스터디

9.1 Cloudflare Workers

Cloudflare Workers는 Wasm을 서버리스 플랫폼으로 활용한 최초의 대규모 사례다.

아키텍처:

요청 → Cloudflare PoPV8 Isolate (Wasm 또는 JS)
       Wasm 코드 실행 (5 ms cold start)
       응답

핵심 인사이트: V8 Isolate를 재활용. 각 요청마다 새 프로세스/VM을 시작하는 대신, V8의 경량 격리 컨텍스트를 씀. Cold start가 5ms 미만 (vs AWS Lambda의 100ms-10s).

한계:

  • CPU 제한 (request당 50ms).
  • 메모리 제한 (128MB).
  • 디스크 없음.
  • 장기 연결 없음 (Durable Objects로 해결).

9.2 Fastly Compute@Edge

Fastly는 Wasmtime을 엣지에서 돌린다.

use fastly::{Request, Response, Error};

#[fastly::main]
fn main(req: Request) -> Result<Response, Error> {
    let backend = "my_backend";
    let bereq = req.with_pass(true);
    let beresp = bereq.send(backend)?;
    Ok(beresp)
}

특징:

  • Cold start: 35 μs (마이크로초).
  • 요청당 인스턴스 생성.
  • Serialize 가능한 immutable 모듈 → 메모리 공유.

35μs는 "인스턴스 하나 만드는 데 걸리는 시간"이지 실행 시간이 아니다. Cloudflare의 5ms보다 100배 빠름.

9.3 Shopify Functions

Shopify는 체크아웃 로직을 고객이 커스터마이징할 수 있게 하고 싶었다. 기존 접근:

  • JavaScript: 안전하지만 속도 불만.
  • Ruby/Python: 너무 위험.
  • Wasm: 안전 + 빠름.

Shopify Functions는:

  • Rust/JavaScript로 작성.
  • Wasm으로 컴파일.
  • Shopify 서버에서 Wasmtime으로 실행.
  • 매 요청마다 새 인스턴스 (깨끗한 상태).
  • 실행 제한 시간 5 ms.

예시: 고객이 특정 조건에서 10% 할인을 적용하는 코드를 작성 → Shopify가 이를 체크아웃 플로우에서 실행.

9.4 Envoy WASM Filters

Istio의 사이드카 프록시인 Envoy는 기본 필터 외에 WASM 필터를 지원한다.

#[no_mangle]
pub fn _start() {
    proxy_wasm::set_log_level(LogLevel::Info);
    proxy_wasm::set_http_context(|_, _| -> Box<dyn HttpContext> {
        Box::new(MyHttpFilter)
    });
}

struct MyHttpFilter;
impl HttpContext for MyHttpFilter {
    fn on_http_request_headers(&mut self, _: usize, _: bool) -> Action {
        self.set_http_request_header("x-wasm", Some("yes"));
        Action::Continue
    }
}

장점:

  • 언어 독립 (Rust, Go, C++, AssemblyScript 등).
  • 핫 리로드 (Envoy 재시작 없이).
  • 샌드박스 (필터가 Envoy 크래시시키지 못함).

Istio/Solo.io 등이 이를 기반으로 확장 생태계를 구축 중.


10. 컨테이너 런타임 통합

10.1 Docker + Wasm

2023년 Docker Desktop이 Wasm 지원을 추가.

docker run --runtime=io.containerd.wasmedge.v1 \
           --platform=wasi/wasm \
           wasmedge/example-wasi:latest \
           /wasi_example_main.wasm 50000000
  • RuntimeClass로 컨테이너 런타임을 Wasm 런타임(WasmEdge)으로 대체.
  • Docker 이미지의 layer에 Wasm 바이너리 저장.
  • Linux 커널, cgroups, namespaces 없이 실행 → 매우 가벼움.

10.2 Kubernetes RuntimeClass

apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: wasmedge
handler: wasmedge
---
apiVersion: v1
kind: Pod
metadata:
  name: my-wasm-app
spec:
  runtimeClassName: wasmedge
  containers:
  - name: app
    image: registry/myapp-wasm:v1

같은 클러스터에서 Linux 컨테이너와 Wasm 컨테이너를 섞어 돌릴 수 있다.

10.3 왜 Wasm 컨테이너?

장점:

  • Cold start 수십 μs (vs Linux container 수백 ms).
  • 이미지 크기 작음 (수 MB).
  • 아키텍처 독립 (x86/ARM 구분 없음).
  • 강력한 샌드박스 (runc보다 더).

한계:

  • Linux 바이너리 그대로 돌릴 수 없음 (Wasm으로 재컴파일 필요).
  • 네트워크/파일 API 제약.
  • 디버깅 도구 부족.

"모든 워크로드가 Wasm이 될 수는 없지만, 사이드카, 함수, 플러그인은 Wasm이 더 낫다"는 게 현재 컨센서스.


11. 언어 지원 현황

11.1 1급 지원 (공식 타겟)

  • C / C++: clang --target=wasm32-wasi. 표준 라이브러리 포함.
  • Rust: cargo build --target wasm32-wasi. 거의 모든 crate 호환.
  • AssemblyScript: TypeScript 서브셋. Wasm 전용 언어.
  • Zig: 우수한 Wasm 지원.
  • Grain: Wasm 우선 함수형 언어.

11.2 실험적/부분 지원

  • Go: GOOS=wasip1 GOARCH=wasm. 1.21부터 WASI 지원. 하지만 런타임 크기가 크다(~2MB).
  • .NET: Blazor가 Wasm을 타겟. .NET 런타임을 Wasm으로 컴파일.
  • Python: CPython을 Wasm으로 컴파일 (Pyodide). 기능 풍부하지만 크기 10+ MB.
  • Ruby: mruby, ruby.wasm. 제한적.
  • Swift: SwiftWasm 프로젝트.
  • Java: TeaVM, CheerpJ. 실험적.

11.3 해결된 GC 문제

오래도록 문제였다: Wasm은 GC가 없다. Java/C#/Go 같은 GC 언어를 Wasm으로 컴파일하려면 런타임 자체를 Wasm으로 컴파일해야 했다 → 크기 폭발.

Wasm GC proposal (2023 안정화)이 해결:

  • Wasm 자체에 GC 지원 추가.
  • ref.null, ref.func, 구조체/배열 타입.
  • 호스트 GC (JavaScript의 V8 GC 등)와 통합.

결과: Kotlin, OCaml, Scheme, Scala.js 등이 "GC 런타임 없는" Wasm 바이너리를 만들 수 있게 됐다.


12. 보안 고려사항

12.1 샌드박스는 얼마나 강한가

Wasm은 메모리 안전제어 흐름 안전을 보장한다. 하지만:

  • Spectre: 투기 실행 기반 사이드 채널 공격. Wasm도 취약. 브라우저는 SharedArrayBuffer 격리로 완화.
  • Row Hammer: DRAM 하드웨어 취약점. 샌드박스로 막기 어려움.
  • Timing attacks: 정확한 타이머가 있으면 가능. 브라우저는 타이머 해상도 낮춤.

"완벽한" 샌드박스는 없지만, 현재 주류 기술 중 가장 강한 격리를 제공하는 건 맞다.

12.2 호스트 함수의 위험

Wasm 코드가 호스트 함수를 호출할 때 주의.

linker.func_wrap("env", "exec_command", |cmd: i32| {
    // cmd는 offset. 호스트는 메모리에서 문자열 읽어 system(cmd) 실행
    // 이건 샌드박스를 무의미하게 만듦!
});

호스트 함수는 Wasm이 "바깥 세상과 상호작용하는 유일한 통로". 잘못 설계하면 보안 구멍이 된다. Principle of Least Privilege: 최소 권한만 제공.

12.3 WASI Capability는 만능이 아니다

Capability 기반이라도 버그가 있을 수 있다. 예: 2023년 Wasmtime에서 경로 탐색 버그로 preopened_dir 밖 접근 가능했던 취약점(CVE-2023-26489). 빠르게 패치됐지만 "진흙 속 교훈".


13. 디버깅과 프로파일링

13.1 소스 맵

clang --target=wasm32-wasi -g로 컴파일하면 DWARF 디버그 정보가 포함된다. wasm-tools 또는 wasmtime --debug-info로 활용 가능.

Chrome DevTools는 Wasm 디버깅을 네이티브 지원한다:

  • 브레이크포인트
  • 변수 inspection
  • 콜 스택

13.2 프로파일링

wasmtime compile --profile=jitdump my_module.wasm
perf record wasmtime run my_module.wasm
perf report

Linux perf로 Wasm의 hot functions를 확인할 수 있다.

13.3 Logging

가장 간단한 방법: stderr에 출력. 호스트가 받아 로그.

eprintln!("debug: x = {}", x);  // WASI stderr

또는 호스트 함수로 구조화 로깅:

#[link(wasm_import_module = "log")]
extern "C" {
    fn log(level: u32, msg_ptr: *const u8, msg_len: u32);
}

14. 실전 팁과 함정

14.1 크기 최적화

기본 Rust → Wasm 바이너리는 수 MB. 줄이려면:

[profile.release]
opt-level = "z"      # 크기 우선
lto = true           # Link-time optimization
codegen-units = 1    # 더 공격적
panic = "abort"      # 패닉 언와인딩 제거
strip = true         # 디버그 심볼 제거

그 후 wasm-opt -Os:

wasm-opt -Os -o out.wasm in.wasm

100KB 이하로 줄일 수 있다.

14.2 인터페이스 오버헤드

Wasm ↔ 호스트 경계는 빠르지만 공짜는 아니다. 특히:

  • 문자열 복사: guest 메모리에서 host 메모리로 복사.
  • 함수 호출: 수십 ns.
  • 배열 복사: 크기에 비례.

핫 루프에서 매 반복마다 호스트 함수 호출은 피할 것. 배치로 처리.

14.3 floats와 Determinism

Wasm의 f32/f64IEEE 754. 하지만 플랫폼별로 미묘한 차이가 있을 수 있다(FMA, denormal 처리 등). 결정적 실행이 필요하면 -fno-associative-math 같은 플래그 사용.

14.4 Memory64 proposal

MVP Wasm은 32-bit 주소만 지원 (4 GB 제한). Memory64 proposal은 64-bit 주소로 확장:

(memory i64 1 65536)  ;; 64-bit indexed memory

2024년 안정화. 큰 데이터셋 처리 시 필수. 단, 메모리 사용량이 약간 증가.


15. 미래와 트렌드

15.1 GC 안정화

GC proposal이 2023년 phase 4(stable) 도달. Kotlin, Scheme 등이 활용 시작. 2025년에는 더 많은 GC 언어가 Wasm을 타겟으로 삼을 것.

15.2 Component Model Ecosystem

  • wasmtime serve: Component를 HTTP 서버로 실행.
  • spin: Fermyon의 Wasm 서버리스 런타임. Component Model 기반.
  • WARG: Wasm 패키지 레지스트리 (npm의 Wasm 버전).

15.3 Wasm AI

Wasm에서 ML 추론을 돌리는 프로젝트가 급증:

  • wasi-nn: 표준 인터페이스.
  • Llamafile: LLaMA 모델을 Wasm으로 패키징.
  • candle-wasm: Rust candle 라이브러리의 Wasm 타겟.

브라우저에서 작은 LLM을 돌리는 것이 현실화.

15.4 커널 내 Wasm?

eBPF와 유사한 접근이 논의 중. "커널 확장을 Wasm으로 작성하면?" 아직 연구 단계지만 흥미로운 방향.


16. 학습 로드맵

1단계: 개념

  • WebAssembly.org — 공식.
  • "WebAssembly for Beginners" — 각종 튜토리얼.
  • MDN Web Docs의 WebAssembly 섹션.

2단계: 실습

  • Rust + wasm-pack으로 첫 Wasm 모듈 만들기.
  • Wasmtime CLI 설치, 예제 실행.
  • AssemblyScript로 "순수 Wasm" 체험.

3단계: 내부

  • Andreas Rossberg의 "Bringing the Web up to Speed" 논문 (Wasm 공식 사양 저자).
  • "WebAssembly: The Definitive Guide" — Brian Sletten.
  • Wasmtime 소스 코드: cranelift/ 디렉토리.

4단계: 생태계

  • Bytecode Alliance 블로그.
  • "Wasm I/O" 연 컨퍼런스.
  • Fermyon, Wasmer, Fastly의 기술 블로그.

17. 요약 — 한 장 정리

┌─────────────────────────────────────────────────────┐
WebAssembly Cheat Sheet├─────────────────────────────────────────────────────┤
│ 바이너리 포맷:- 섹션 기반, LEB128 인코딩                           │
- 스택 머신 ISA- 구조화된 제어 흐름 (block/loop/if/br)│                                                       │
│ 실행 모델:- Module(코드) + Instance(상태) + Store(환경)- 선형 메모리 (64 KB 페이지 단위)- 호스트 메모리 접근 불가                             │
│                                                       │
│ 검증:- 단일 패스 O(n)- 타입 체크, 스택 깊이, 메모리/테이블 경계            │
│                                                       │
JIT:- Wasmtime: Cranelift (AOT도 가능)- V8: Liftoff (baseline) + TurboFan (optimizing)- 계층적 컴파일: 빠른 시작 + 피크 성능                │
│                                                       │
WASI:- POSIX-like 시스템 인터페이스                        │
- Capability 기반 보안                               │
- Preview 1 (legacy), Preview 2 (Component Model)│                                                       │
Component Model:- WIT (Interface Types) IDL- Canonical ABI로 언어 간 타입 있는 통신              │
- 컴포넌트 구성(composition) 지원                    │
│                                                       │
│ 런타임:- Wasmtime (Rust, Bytecode Alliance)- Wasmer (Rust, 상용)- V8/SpiderMonkey (브라우저)- WasmEdge (C++, CNCF, 엣지)│                                                       │
│ 실전:- Cloudflare Workers, Fastly Compute@Edge- Shopify Functions- Envoy/Istio WASM filters                          │
- Docker + K8s RuntimeClass│                                                       │
│ 언어 지원:- 1: C/C++, Rust, AssemblyScript, Zig- GC proposal로 Kotlin/Scheme/Scala.js 확장        │
└─────────────────────────────────────────────────────┘

18. 퀴즈

Q1. WebAssembly가 구조화된 제어 흐름만 허용하는 이유는?

A. 단일 패스 검증스트리밍 컴파일을 가능하게 하기 위해. 임의의 goto/jmp를 허용하면 검증기가 모든 제어 경로를 추적해야 해서 O(n²) 이상이 된다. block/loop/if/br만 허용하면 스택 깊이와 타입을 순차적으로 추적할 수 있어 O(n) 단일 패스로 검증 완료. 또한 SSA 변환이 쉬워 JIT 컴파일도 빨라진다. 설계자 Ben Titzer의 핵심 결정 중 하나.

Q2. Wasm의 선형 메모리란 무엇이며 왜 안전한가?

A. 각 Wasm 인스턴스가 가지는 하나의 큰 바이트 배열. 64 KB 페이지 단위로 할당, 런타임에 memory.grow로 확장 가능. 모든 포인터는 이 배열의 오프셋(u32)이다. 호스트 메모리에 접근할 방법이 아예 없고, 모든 메모리 접근은 경계 체크(배열 길이와 비교)를 거친다. 이것이 C로 쓴 코드도 Wasm에서 메모리 안전을 보장받는 이유.

Q3. WASI의 Capability 기반 보안이 전통 POSIX와 다른 점은?

A. POSIX는 "프로그램이 아무 경로나 open 시도, 커널이 권한 체크" 모델. WASI는 "호스트가 미리 허용한 리소스만 접근 가능" 모델. 예: 호스트가 preopened_dir("/data")를 제공하면 Wasm 코드는 /data/ 이하만 볼 수 있고 다른 디렉토리는 아예 존재하지 않는 것처럼 보인다. "권한이 있어야 쓸 수 있다"는 최소 권한 원칙. Wasm 코드를 안전하게 실행할 수 있는 핵심 메커니즘.

Q4. V8의 Liftoff가 TurboFan보다 빠른 이유는?

A. 중간 표현(IR)을 거치지 않는다. Wasm 명령어를 바로 머신 코드로 변환하고, 레지스터 할당은 "스택 top을 레지스터에 매핑"하는 단순 전략. 최적화를 거의 하지 않는 대신 컴파일 속도가 1-2 GB/s (TurboFan은 수 MB/s). 덕분에 페이지 로드 시 Wasm이 즉시 실행 가능. 실행 후 백그라운드에서 TurboFan이 최적화를 진행해 핫 함수를 교체. 빠른 시작 + 피크 성능을 동시에.

Q5. Component Model이 해결하는 문제는?

A. 언어 간 타입 있는 인터페이스가 없다는 Wasm의 최대 한계. 초기 Wasm은 C ABI 수준(i32, f64만)의 호환만 가능했다. Rust와 Go Wasm 모듈이 문자열이나 구조체를 주고받으려면 서로 메모리 레이아웃을 동의해야 했다. Component Model은 WIT IDL로 타입을 정의하고 Canonical ABI로 직렬화 규칙을 표준화해서, 모든 언어가 같은 인터페이스를 통해 통신할 수 있게 했다. "Wasm의 gRPC"라고 부를 만하다.

Q6. Cloudflare Workers의 "5ms cold start"는 어떻게 달성되는가?

A. V8 Isolate를 재활용하기 때문. 전통 서버리스(Lambda)는 요청마다 새 컨테이너/VM을 기동하지만, V8 isolate는 경량 격리 컨텍스트다. 프로세스와 V8 엔진이 이미 로딩된 상태에서 isolate만 새로 만들면 수 ms에 끝난다. Wasm 모듈은 파싱/검증 비용이 매우 낮아 isolate 초기화 시간 안에 들어간다. 대신 CPU 시간/메모리 제약이 엄격하고 장기 연결이 어렵다는 트레이드오프.

Q7. Wasm과 eBPF의 공통점과 차이는?

A. 공통점: 둘 다 샌드박스 바이트코드 VM. 둘 다 검증기로 안전 보장. 둘 다 JIT을 통해 네이티브 성능 접근. 차이점: Wasm은 유저 공간(브라우저/서버/엣지)을, eBPF는 커널을 타겟. Wasm은 구조화된 제어 흐름(단일 패스 검증), eBPF는 일반 제어 흐름(symbolic execution). Wasm은 선형 메모리 오프셋, eBPF는 타입 있는 포인터. 결과적으로 Wasm은 "일반 프로그래밍", eBPF는 "커널 확장"에 특화.


이 글이 도움이 됐다면 다음 포스트도 확인해 보세요:

  • "eBPF Deep Dive" — 커널 쪽의 비슷한 접근.
  • "JIT Compilation V8 & JVM 최적화" — TurboFan과 HotSpot 심층 비교.
  • "Rust Tokio Async Runtime" — Wasm의 주요 컴파일 소스 언어.
  • "Container Internals" — Docker+Wasm, Kubernetes RuntimeClass 배경.