LabHub

블로그

WebAssembly와 WASI 완전 해부 — 바이트코드, Sandbox, Component Model, Wasmtime, 엣지 런타임까지

한국어English日本語

들어가며 — "Docker보다 작고 JVM보다 빠른"

2019년 Docker 창시자 Solomon Hykes의 트윗이 화제였다.

"If WASM+WASI existed in 2008, we wouldn't have needed to create Docker."

논란이 많은 발언이었지만, 핵심은 분명했다. Wasm은 그저 브라우저용 기술이 아니다. 2025년 지금, Cloudflare Workers, Fastly Compute@Edge, Shopify Functions, Figma 플러그인, 그리고 심지어 Kubernetes 위의 SpinKube까지 — 전부 Wasm을 엔진으로 쓴다.

이 글에서는:

이전 글 HTTP/3와 QUIC 완전 해부에서 전송 계층의 재설계를 봤다. Wasm은 실행 계층의 재설계다.


1. 탄생 배경 — asm.js가 남긴 숙제

JavaScript의 불운한 수명

2010년대 초, 브라우저는 사실상 "JavaScript만 돌아가는 운영체제"였다. 하지만 JS는:

asm.js (Mozilla, 2013)

답은 JavaScript를 "서브셋"으로 쓰는 asm.js였다.

function add(x, y) {
  x = x|0;  // 비트 OR로 정수 강제 변환
  y = y|0;
  return (x + y)|0;
}

|0 같은 힌트로 타입을 "미리" 확정하면, 브라우저가 기계어 코드로 최적 컴파일 가능. Emscripten이 C/C++ → asm.js로 변환해 브라우저에서 Unreal Engine이 돌았다.

하지만 asm.js는 한계가 분명했다:

Wasm 1.0 (2017, W3C 표준)

4개 브라우저 벤더(Mozilla, Google, Microsoft, Apple)가 모여 공식 바이트코드를 설계. 2019년 W3C 공식 권고. 이름은 WebAssembly.

특징:


2. Wasm 모듈의 해부

최소 예제

(module
  (func $add (param $a i32) (param $b i32) (result i32)
    local.get $a
    local.get $b
    i32.add
  )
  (export "add" (func $add))
)

WAT(WebAssembly Text format)로 작성. 컴파일하면 바이너리 .wasm. 브라우저/런타임에서 add(3, 5) 호출 가능.

모듈의 Section 구조

.wasm 파일은 섹션의 나열이다.

[0] Custom      - 메타데이터 (이름, 디버그 정보)
[1] Type        - 함수 시그니처 목록
[2] Import      - 외부에서 가져오는 항목
[3] Function    - 함수 → 타입 인덱스 매핑
[4] Table       - 함수 포인터 테이블 (동적 호출용)
[5] Memory      - 선형 메모리 정의
[6] Global      - 전역 변수
[7] Export      - 외부에 노출하는 항목
[8] Start       - 모듈 초기화 함수
[9] Element     - Table 초기화
[10] Code       - 함수 본문 (바이트코드)
[11] Data       - Memory 초기화

핵심: 선형 메모리 (Linear Memory)

Wasm 프로그램은 하나의 큰 연속된 바이트 배열을 메모리로 본다. 64KB 단위 페이지. 기본 시작 크기와 최대 크기 지정.

(memory 1 10)  ;; 최소 1 페이지(64KB), 최대 10 페이지

C/C++/Rust가 "포인터"라고 부르는 것은 이 배열의 오프셋(i32)에 불과. 프로그램이 할당/해제를 자체 관리한다 (malloc/free도 Wasm으로 컴파일된 코드).

중요: Wasm은 이 메모리 밖으로 절대 접근할 수 없다. 호스트의 메모리에도, 다른 모듈의 메모리에도. 이게 Sandbox의 1차 방어선이다.

스택 머신

Wasm은 스택 기반 VM.

local.get $a    ;; 스택 push a
local.get $b    ;; 스택 push b
i32.add         ;; 스택에서 둘 pop, 더해서 push

레지스터 기반(예: x86)과 달리, 스택 명령은 단순하고 인코딩이 짧다. 런타임이 이를 기계어로 JIT/AOT 컴파일할 때 최적화 여지가 크다.


3. Sandbox의 수학적 보증

왜 Wasm은 안전한가

Wasm이 제공하는 격리:

  1. 메모리 격리 — 선형 메모리 밖 접근 불가. 호스트도 다른 Wasm 모듈도 못 건드림
  2. Control Flow 무결성 — 함수 포인터는 Table을 통해서만 호출. 임의 주소로 점프 불가
  3. 시스템 콜 없음 — 기본 Wasm은 외부 세계에 직접 접근할 수 없음. Import된 함수만 호출 가능
  4. 결정론적 — 동일 입력 → 동일 출력 (부동소수점 NaN 비트 규격화 등으로 결정성 확보)

Capability 기반 보안

컨테이너는 "모든 기본 권한 ON → 취약한 것만 OFF"(deny-list 방식). Wasm은 반대. "아무 것도 못 함 → 명시적으로 Capability 전달하면 가능" (allow-list, Capability 기반).

예: 파일 읽기. Wasm 모듈은 기본적으로 파일시스템에 접근 불가. 호스트가 fd_read 함수를 import에 제공했다면, 호스트가 어떤 fd를 열어줬는지까지만 접근 가능. 경로 조작(../etc/passwd)도 호스트가 root fd를 어떤 디렉토리로 줬는지에 따라 제한.

컨테이너 vs Wasm 보안 비교

측면컨테이너Wasm
격리 방식Linux namespace/cgroup가상머신 + 타입 안전
기본 권한많음 (deny-list)없음 (allow-list)
탈출 공격 면커널 CVE (예: Dirty COW)VM 명세 수학적 검증
시스템 콜수백 개호스트가 import한 것만
시작 시간100ms~수초1ms 미만

실제로 Fastly는 Wasm에서 발견된 sandbox 탈출을 "다른 Wasm 모듈을 공격" 수준도 아닌 "Wasmtime 버그로 OOM"으로만 경험한다고 보고.


4. WASI — 브라우저 밖으로 나가다

문제: 브라우저 API만으론 서버에서 못 씀

초기 Wasm은 브라우저 API (fetch, DOM) 의존. 서버에서 파일을 읽거나 소켓을 열 표준 방법이 없었다.

WASI 0.1 (2019) — POSIX-ish

Mozilla의 Lin Clark가 주도. POSIX처럼 보이는 시스템 인터페이스를 Wasm용으로.

wasi_snapshot_preview1
  - fd_read / fd_write
  - fd_seek
  - path_open
  - clock_time_get
  - random_get
  - args_get / environ_get

특징: 파일 디스크립터가 호스트가 명시적으로 준 것만 쓸 수 있음 (Capability).

WASI 0.2 (2024) — Component Model 기반

0.1의 한계:

0.2는 Component Model 위에 재구축.

언어 중립 인터페이스 정의 (WIT).

WIT 예시

package wasi:http@0.2.0;

interface types {
  variant method {
    get,
    post,
    put,
    delete,
    other(string),
  }
  record request {
    method: method,
    uri: string,
    headers: list<tuple<string, string>>,
    body: option<body>,
  }
}

이 인터페이스는 Rust, Go, JavaScript, Python 각각의 바인딩으로 생성 가능. "언어 간 표준 API".


5. Component Model — 언어 중립의 비밀

왜 모듈만으론 부족한가

Wasm 1.0 모듈은 숫자와 바이트만 주고받는다. 문자열, 배열, 구조체는 선형 메모리 오프셋으로 직접 조율해야 한다. 언어마다 규칙이 달라 상호운용이 악몽.

예: Rust → JavaScript로 문자열 전달

Component Model (2024 안정화)

Wasm 위에 "컴포넌트"라는 상위 단위. 컴포넌트 간:

핵심 이점

  1. 언어 X로 만든 컴포넌트언어 Y로 만든 컴포넌트에 꽂아 사용
  2. 버전 관리 — WIT 인터페이스의 semver
  3. Registry — warg.io 같은 컴포넌트 패키지 레지스트리 출현

실제 사례:

npm + Docker + gRPC의 결합이라는 평가.


6. Wasm 런타임 3대장

Wasmtime (Bytecode Alliance)

Wasmer

WasmEdge (CNCF Sandbox)

성능 비교 (2025 Bytecode Alliance 벤치)

런타임StartupThroughputMemory
Wasmtime (Cranelift)0.3ms100%기준
Wasmer (LLVM)2s 컴파일 / 0.1ms 재실행115%1.3x
WasmEdge0.5ms95%0.8x

선택 기준:


7. Cloudflare Workers — Wasm이 아닌 Isolate, 그러나 Wasm도

흔한 오해

"Cloudflare Workers = Wasm"이라고 알고 있는 경우가 많다. 실은:

왜 Isolate + Wasm인가

Fastly Compute@Edge — 완전 Wasm

Fastly는 전적으로 Wasm 기반.

1밀리초 미만 콜드 스타트가 가능한 이유: Wasm AOT 컴파일 결과를 재사용하고, Sandbox가 가볍기 때문.


8. Kubernetes에서 Wasm 돌리기

runwasi 프로젝트

containerd의 shim으로 들어가 OCI 이미지로 패키징된 Wasm을 컨테이너처럼 실행. 기존 Kubernetes 생태계에 그대로 얹힘.

apiVersion: v1
kind: Pod
spec:
  runtimeClassName: wasmtime
  containers:
  - image: ghcr.io/example/hello-wasm:latest
    name: hello

SpinKube (Fermyon, 2024)

Spin은 Fermyon의 Wasm 프레임워크. SpinKube는 Kubernetes CRD로 Wasm 앱을 관리.

장점:

단점:

KWasm (Liquid Reply)

DaemonSet으로 Wasm 런타임을 노드에 설치. 기존 Kubernetes 클러스터를 Wasm 지원 환경으로 업그레이드.


9. 언어별 Wasm 여정

Rust — 황금 표준

Go — TinyGo가 구원

JavaScript/TypeScript — AssemblyScript

Python — Pyodide, py2wasm

C/C++ — Emscripten, wasi-sdk


10. 성능 — "네이티브의 80%"가 진짜인가

이론

Wasm 바이트코드는 실행 시 JIT/AOT로 기계어. 같은 C 소스를:

지연 요소들

  1. 경계 검사 — 선형 메모리 접근마다 bounds check. 현대 런타임은 OS의 가상 메모리 트릭(mmap + guard pages)으로 제로 오버헤드에 근접
  2. SIMD — Wasm SIMD 128비트 제안이 표준화. Rust의 std::simd 일부를 Wasm에 매핑
  3. Atomics + Threads — 공유 메모리와 아토믹 연산 지원. 웹워커로 병렬 실행
  4. GC 제안 — 호스트 GC에 의존하지 않는 Wasm용 GC. TypeScript/Kotlin/Java를 효율적으로 Wasm으로

현실 — 병목은 FFI

"Wasm이 네이티브보다 느리다"는 벤치마크들, 파고 보면 대부분 FFI 횟수가 많은 케이스다. 호스트 함수를 자주 호출하면 boundary 전환 비용. 해결:


11. 실전 사례 — 누가 어떻게 쓰는가

Figma

플러그인 시스템을 QuickJS + Wasm으로. 서드파티 코드를 브라우저에서 안전하게 실행. Plugin API 제한된 capability만 노출.

Shopify Functions

서드파티 앱이 체크아웃 로직을 커스터마이즈 가능. Rust → Wasm으로 업로드. 지연이 극도로 민감한 영역이라 Wasm의 빠른 시작이 필수.

Adobe Photoshop Web

Photoshop 데스크톱 코드를 C++에서 Wasm으로 빌드. 브라우저에서 실제 Photoshop 커널이 돌아감. Emscripten 활용.

1Password

브라우저 확장의 암호화 모듈을 Rust → Wasm으로. 보안 감사와 성능을 동시 달성.

Envoy Proxy

WASM 필터로 동적 확장. Lua/Go 필터보다 성능 좋고 언어 선택 자유. Istio의 기본 확장 메커니즘.


12. 함정과 현실적 한계

한계 1: Thread 모델

Wasm의 병렬성은 여전히 제한적. 공유 메모리 원자 연산은 가능하지만:

한계 2: 네트워킹

WASI 0.2가 wasi:sockets 도입했지만:

한계 3: 파일시스템 일관성

"Capability 기반 fd"는 안전하지만:

한계 4: 디버깅

한계 5: 바이너리 크기

"경량"이라지만:

엣지에서 콜드 스타트하려면 수십 KB가 목표인데 Rust는 panic! 체인만 해도 덩치.


13. 보안 고려사항

1. Wasm 자체는 안전하지만 import는 위험

모듈이 fd_write를 imported했다면, 호스트가 안전한 구현을 제공해야 한다. "Wasm이라서 안전"은 틀리고, "Wasm 런타임과 호스트 바인딩이 안전해야"가 정확.

2. Denial of Service

3. Side Channel

4. Supply Chain


14. 미래 — 어디로 가는가

임박한 표준들

Trend 예측

  1. 서버리스 네이티브 — Lambda처럼 기동이 중요한 영역이 Wasm으로 이동
  2. 플러그인 시스템 공통 언어 — Kafka Connect, VSCode 확장 등이 Wasm 기반으로 재탄생 중
  3. Edge AI — WasmEdge + ONNX로 모델 추론을 엣지로
  4. Kubernetes 포드 대안 — Wasm 워크로드가 Pod의 일부 영역 대체
  5. 브라우저 + 서버 통합 — 같은 Wasm 모듈을 양쪽에서 실행

한계의 실명


15. 실무 체크리스트 12가지

  1. 사용 사례가 명확할 때만 도입 — 플러그인, 서버리스, 클라이언트 고성능
  2. 컴파일러 옵션으로 바이너리 크기 최적화--release, wasm-opt -Os, strip
  3. WASI 0.2 Component Model 기반 선택 — 미래 호환성
  4. Wasmtime을 레퍼런스 런타임으로 — 표준 구현 확인
  5. Capability 최소 원칙 — 필요한 fd만 열어줌
  6. Fuel / Epoch로 실행 시간 제한 — DoS 방어
  7. wasm-opt 체인 — 크기 30%+ 감소
  8. 언어 선택은 팀 역량 기반 — Rust가 가장 성숙, TinyGo는 Go 친화
  9. hot path는 Wasm, cold path는 JS/호스트 — 혼합 설계
  10. 관측성 — 호스트가 로그/메트릭 제공 — Wasm 내부 자체 로깅은 제한
  11. 메모리 한계 + OOM 핸들링 — 호스트에서 감싸기
  12. CI에서 여러 런타임 테스트 — Wasmtime, Wasmer, WasmEdge 호환성

다음 글 예고 — CDN과 엣지 컴퓨팅의 아키텍처

Wasm이 "서버 없는 서버"의 엔진이라면, 그걸 전 세계에 뿌리는 인프라가 CDN과 엣지 네트워크다. 다음 글에서는:

"사용자가 1초 안에 페이지를 본다"는 목표 뒤에 얼마나 많은 계층이 있는지, 거기서 Wasm이 어떻게 역할을 하는지 이어서 보자.

"인터넷은 평평하지 않다. 지구의 곡률을 따라 CDN이 깔려 있고, 그 위에서 Wasm이 뛰어다닌다."

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다