Skip to content

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

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

들어가며 — "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을 엔진으로 쓴다.

이 글에서는:

  • WebAssembly의 탄생 — asm.js의 그림자에서 Wasm 1.0까지
  • 바이트코드 구조와 스택 가상머신 실행 모델
  • Sandbox의 수학적 보증 — 왜 컨테이너보다 안전한가
  • WASI — 브라우저 밖의 Wasm 시스템 인터페이스
  • Wasmtime / Wasmer / WasmEdge 런타임 비교
  • Component Model — 언어 중립 모듈 시스템의 비밀
  • Cloudflare Workers가 왜 Isolate 대신 Wasm도 지원하는가
  • Kubernetes + Wasm — runwasi, SpinKube의 그림
  • 실무 함정과 미래

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


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

JavaScript의 불운한 수명

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

  • 동적 타입 → JIT이 타입 가정을 끊임없이 재확인
  • 가비지 컬렉션 지연
  • 고성능 수치 연산에 부적합

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의 한계:

  • 오직 "동기 POSIX" 모델
  • HTTP/sockets 표준 부재
  • 인터페이스가 C 스타일로 고정

0.2는 Component Model 위에 재구축.

  • wasi:http — HTTP 요청/응답 처리 표준
  • wasi:sockets — TCP/UDP
  • wasi:io/streams — 비동기 스트림
  • wasi:cli — CLI 앱용 stdio

언어 중립 인터페이스 정의 (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로 문자열 전달

  • Rust는 UTF-8 길이+포인터
  • JS는 UTF-16
  • 변환 코드를 손으로 썼다

Component Model (2024 안정화)

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

  • 고수준 타입 (string, record, variant, list, option, result)
  • 자동 ABI 변환
  • import/export를 WIT로 선언

핵심 이점

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

실제 사례:

  • Rust로 만든 이미지 처리 컴포넌트를
  • Go로 만든 HTTP 핸들러에서 import해서
  • JavaScript로 만든 오케스트레이터가 조립

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


6. Wasm 런타임 3대장

Wasmtime (Bytecode Alliance)

  • 기본 엔진: Cranelift (Rust 컴파일러 프로젝트)
  • 언어: Rust
  • 특징: WASI 표준 구현의 레퍼런스
  • 안정성, 보안 감사 품질 최상
  • wasmtime run module.wasm 한 줄로 실행
  • Fastly Compute가 백엔드로 사용

Wasmer

  • 언어: Rust
  • 컴파일러 플러그러블: Singlepass(빠른 JIT), Cranelift, LLVM(최고 성능)
  • wapm 패키지 매니저
  • wasmer run 명령, 임베딩 API 다양 (C, Go, Python 등)
  • 클라우드 기반 Wasmer Edge 제공

WasmEdge (CNCF Sandbox)

  • 언어: C++
  • 특징: AI 워크로드 특화 — Tensor 연산, ONNX 지원
  • Docker + Wasm 통합(runwasi)에 활발
  • 많은 임베디드/IoT 환경 지원

성능 비교 (2025 Bytecode Alliance 벤치)

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

선택 기준:

  • 표준 준수 → Wasmtime
  • 극한 성능 → Wasmer LLVM
  • 작은 메모리 + AI → WasmEdge

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

흔한 오해

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

  • 기본은 V8 Isolate — JavaScript 런타임을 경량 격리 단위로 분리
  • Isolate 하나 = 약 3MB 메모리, 5ms 콜드 스타트
  • Wasm은 Isolate 안에서 실행 가능

왜 Isolate + Wasm인가

  • JavaScript 생태계 그대로 씀 (npm)
  • 성능 크리티컬 경로는 Wasm으로
  • 예: Cloudflare Pages가 Rust → Wasm으로 이미지 변환

Fastly Compute@Edge — 완전 Wasm

Fastly는 전적으로 Wasm 기반.

  • 각 요청이 새 Wasm 인스턴스
  • 언어: Rust, Go (TinyGo), JS, AssemblyScript
  • 콜드 스타트 35μs (마이크로초!)

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 앱을 관리.

장점:

  • Pod이 수 밀리초에 뜸
  • Deno/Node보다 훨씬 작은 메모리
  • Serverless가 "항상 살아있는 것처럼" 반응

단점:

  • 디버깅 도구 성숙 부족
  • 모든 라이브러리가 Wasm 타겟 빌드되진 않음
  • WASI 0.2 네이티브 지원이 아직 과도기

KWasm (Liquid Reply)

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


9. 언어별 Wasm 여정

Rust — 황금 표준

  • cargo build --target wasm32-wasi
  • wasm-bindgen로 브라우저 JS FFI
  • wit-bindgen로 Component Model
  • 크레이트 생태계의 상당 부분이 Wasm 지원

Go — TinyGo가 구원

  • 표준 Go 컴파일러는 큰 런타임 포함 (GC, goroutine)
  • Go 1.21부터 GOOS=wasip1 GOARCH=wasm로 WASI 공식 지원
  • 하지만 바이너리 크기가 여전히 몇 MB
  • TinyGo는 LLVM 기반으로 훨씬 작은 출력 (수십 KB)
  • 단점: 표준 라이브러리 일부 미지원

JavaScript/TypeScript — AssemblyScript

  • TypeScript 문법으로 Wasm 작성
  • 빠른 학습 곡선
  • 그러나 풀 JS 생태계는 못 씀

Python — Pyodide, py2wasm

  • 브라우저에서 Python 돌리는 Pyodide
  • CPython 자체를 Wasm으로 컴파일 (10MB+)
  • 서버리스용으론 무겁지만 교육/데이터 시각화 유용

C/C++ — Emscripten, wasi-sdk

  • 원래 asm.js 시대부터 지원
  • Unity/Unreal 엔진도 Wasm 빌드

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

이론

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

  • Native (gcc) vs Wasm (clang → wasm → Wasmtime AOT)
  • 대부분 벤치마크에서 80~95% 네이티브 속도

지연 요소들

  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 전환 비용. 해결:

  • Wasm 내부에서 최대한 처리
  • 배치 호출
  • 공유 메모리 기반 IPC

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의 병렬성은 여전히 제한적. 공유 메모리 원자 연산은 가능하지만:

  • 웹워커 기반이라 메시지 기반 통신이 기본
  • 리턴/재진입 불가한 런타임 많음
  • goroutine/async Rust의 풍부함 대비 부족

한계 2: 네트워킹

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

  • 브라우저에서는 여전히 fetch API만 (CORS/Same-Origin)
  • 서버 Wasm에서도 라이브러리 지원이 제각각

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

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

  • 경로 기반 코드 대량 포팅 번거로움
  • POSIX fork()/exec() 부재 (의도적)

한계 4: 디버깅

  • DWARF 지원 향상 중이지만 gdb/lldb 경험은 아직 네이티브 수준 아님
  • Chrome DevTools가 Wasm 디버깅 제공하지만 소스맵 한계
  • 스택 트레이스 읽기 난해

한계 5: 바이너리 크기

"경량"이라지만:

  • Rust hello world: ~200KB (wasm32-wasi)
  • TinyGo: ~50KB
  • AssemblyScript: ~10KB
  • Python/CPython: ~10MB

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


13. 보안 고려사항

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

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

2. Denial of Service

  • 무한 루프: Fuel 또는 Epoch 기반 인터럽트로 제한
  • 메모리 폭주: Memory limit 명시
  • 스택 오버플로: Wasm 명세가 제한 지원

3. Side Channel

  • Spectre/Meltdown 류 타이밍 공격은 Wasm 내부에서도 가능
  • 런타임이 timing-precision 제한 (1ms 단위로 라운딩 등)

4. Supply Chain

  • 컴포넌트 레지스트리(warg)의 서명 검증 필수
  • WIT 인터페이스 호환성 검증

14. 미래 — 어디로 가는가

임박한 표준들

  • WASI 0.3 — 비동기/스트림 더 풍부
  • Exception Handling — try/catch 표준화
  • Stack Switching — 언어별 코루틴/async
  • GC Proposal — Wasm GC (이미 Chrome 119+/Firefox 120+ 지원)
  • Relaxed SIMD — 플랫폼별 SIMD 최적화

Trend 예측

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

한계의 실명

  • JavaScript 생태계의 중력 — 당분간 JS 대체 X
  • POSIX 코드베이스의 막대함 — 전부 Wasm으로 못 옮김
  • 디버깅/관측성 성숙도 — 2~3년은 더 필요

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과 엣지 네트워크다. 다음 글에서는:

  • CDN의 탄생 — Akamai 1998년부터 Cloudflare, Fastly까지
  • Anycast 라우팅의 마법
  • 캐시 계층 전략 — tiered, pull-through, push
  • 엣지 컴퓨팅의 의미 — Lambda@Edge vs Workers vs Compute@Edge
  • Cache Invalidation — "컴퓨터 과학의 두 난제 중 하나"
  • DDoS 방어 — L3/L4/L7, Rate Limiting, Bot Management
  • 이미지/비디오 변환의 엣지 처리
  • Zero Trust 네트워크 — Cloudflare Access, Access Zero Trust
  • 상위 CDN 벤더 비교 (Cloudflare/Akamai/Fastly/AWS CloudFront)

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

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

현재 단락 (1/283)

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

작성 글자: 0원문 글자: 9,684작성 단락: 0/283