WebAssembly 컴포넌트 모델의 필요성, WIT IDL의 문법과 타입 시스템, 인터페이스와 월드 정의, 컴포넌트 구성을 통한 언어 간 상호운용성을 다룹니다.
WebAssembly의 코어 스펙은 의도적으로 저수준입니다. 함수가 주고받을 수 있는 타입은 i32, i64, f32, f64 네 가지 숫자 타입뿐입니다. 문자열을 전달하려면 메모리에 바이트를 쓰고, 그 주소와 길이를 정수로 넘겨야 합니다.
이 저수준 인터페이스는 두 가지 심각한 문제를 야기합니다.
첫째, 상호운용이 극도로 어렵습니다. Rust로 컴파일한 Wasm 모듈이 문자열을 UTF-8로 인코딩하고, Python으로 컴파일한 모듈이 UTF-16을 사용한다면, 두 모듈은 직접 소통할 수 없습니다. ABI(Application Binary Interface) 호환성이 보장되지 않기 때문입니다.
둘째, 인터페이스 계약이 없습니다. 코어 Wasm 모듈의 익스포트를 보면 func(i32, i32) -> i32 같은 시그니처만 보입니다. 이 함수가 어떤 구조체를 기대하는지, 에러를 어떻게 반환하는지 알 방법이 없습니다.
**컴포넌트 모델(Component Model)**은 이 문제들을 해결하기 위해 코어 Wasm 위에 고수준 타입 시스템과 표준 ABI를 추가합니다.
**WIT(WebAssembly Interface Type)**는 컴포넌트의 인터페이스를 정의하는 IDL(Interface Description Language)입니다. Protocol Buffers나 GraphQL 스키마와 비슷한 역할을 하지만, WebAssembly에 특화된 타입 시스템을 제공합니다.
WIT는 다음과 같은 기본 타입을 제공합니다.
| 분류 | 타입 |
|---|---|
| 부울 | bool |
| 부호 있는 정수 | s8, s16, s32, s64 |
| 부호 없는 정수 | u8, u16, u32, u64 |
| 부동소수점 | f32, f64 |
| 문자 | char |
| 문자열 | string |
기본 타입을 조합하여 풍부한 데이터 구조를 표현할 수 있습니다.
package example:blog@1.0.0;
interface types {
// 레코드 — 이름이 붙은 필드의 집합 (struct)
record post {
id: u64,
title: string,
content: string,
author: string,
published: bool,
tags: list<string>,
created-at: u64,
}
// 배리언트 — 여러 케이스 중 하나 (tagged union / enum)
variant sort-order {
newest,
oldest,
most-viewed,
by-title(sort-direction),
}
// 열거형 — 값 없는 배리언트의 축약
enum sort-direction {
ascending,
descending,
}
// 옵션 타입 — 값이 있거나 없음
// option<T>는 내장 타입
// 결과 타입 — 성공 또는 실패
// result<T, E>는 내장 타입
// 플래그 — 비트 플래그 집합
flags permissions {
read,
write,
execute,
}
// 튜플
type point = tuple<f64, f64>;
}WIT의 네이밍 컨벤션은 kebab-case입니다. sortOrder나 sort_order가 아닌 sort-order를 사용합니다. 각 언어의 코드 생성기가 해당 언어의 컨벤션으로 자동 변환합니다. 예를 들어, sort-order는 Rust에서 SortOrder, Python에서 sort_order가 됩니다.
**리소스(Resource)**는 컴포넌트 모델에서 가장 강력한 타입 중 하나입니다. 불투명한 핸들로, 소유권과 수명을 추적합니다. 데이터베이스 연결, 파일 핸들 등 상태가 있는 객체를 표현하는 데 적합합니다.
interface database {
// 리소스 정의 — 불투명 핸들
resource connection {
// 생성자
constructor(url: string);
// 메서드 (self 참조)
query: func(sql: string) -> result<list<row>, db-error>;
// 정적 메서드
pool-size: static func() -> u32;
}
record row {
columns: list<column-value>,
}
variant column-value {
integer(s64),
float(f64),
text(string),
null,
}
variant db-error {
connection-failed(string),
query-failed(string),
timeout,
}
}리소스는 **소유권 의미론(Ownership Semantics)**을 따릅니다. 리소스를 함수에 전달하면 소유권이 이전되며, 빌려주려면 borrow<connection> 타입을 사용합니다. Rust의 소유권 모델에서 영감을 받은 설계입니다.
인터페이스는 관련된 함수와 타입의 모음입니다. 하나의 WIT 파일에 여러 인터페이스를 정의할 수 있습니다.
package example:blog-service@1.0.0;
interface reader {
use types.{post, sort-order};
get-post: func(id: u64) -> option<post>;
list-posts: func(
page: u32,
per-page: u32,
order: sort-order,
) -> list<post>;
search: func(query: string) -> list<post>;
}
interface writer {
use types.{post};
record create-post-input {
title: string,
content: string,
tags: list<string>,
}
create-post: func(input: create-post-input) -> result<post, string>;
update-post: func(id: u64, input: create-post-input) -> result<post, string>;
delete-post: func(id: u64) -> result<_, string>;
}**월드(World)**는 컴포넌트의 전체 인터페이스를 정의합니다. 어떤 인터페이스를 가져오고(import), 어떤 인터페이스를 내보내는지(export) 명시합니다.
world blog-api {
// 이 컴포넌트가 사용하는 인터페이스 (호스트 제공)
import wasi:logging/logging;
import wasi:keyvalue/store;
// 이 컴포넌트가 제공하는 인터페이스
export reader;
export writer;
}컴포넌트 모델의 핵심 기반은 Canonical ABI입니다. WIT에서 정의한 고수준 타입이 코어 Wasm의 저수준 타입으로 어떻게 변환되는지 규정합니다.
예를 들어, string 타입은 Canonical ABI에서 다음과 같이 처리됩니다.
이 변환 과정은 개발자가 직접 다룰 필요가 없습니다. WIT에서 코드를 생성하면 바인딩 코드가 자동으로 Canonical ABI를 처리합니다.
Canonical ABI의 자동 처리 덕분에, Rust로 작성한 컴포넌트가 내보내는 func(input: string) -> result<post, error> 함수를 Python으로 작성한 다른 컴포넌트가 아무런 수동 작업 없이 호출할 수 있습니다. 이것이 컴포넌트 모델의 핵심 가치입니다.
컴포넌트 모델의 가장 강력한 기능 중 하나는 **컴포넌트 구성(Component Composition)**입니다. 여러 컴포넌트를 조합하여 새로운 컴포넌트를 만들 수 있습니다. 각 컴포넌트가 다른 언어로 작성되어 있어도 상관없습니다.
wasm-tools compose 명령어를 사용하여 컴포넌트를 구성할 수 있습니다.
# 개별 컴포넌트 빌드
cargo component build --manifest-path auth/Cargo.toml
componentize-py -w business-logic business_logic.py -o business.wasm
tinygo build -target=wasip2 -o cache.wasm cache/main.go
# 컴포넌트 구성
wasm-tools compose \
auth/target/wasm32-wasip2/release/auth.wasm \
--definitions business.wasm \
--definitions cache.wasm \
-o composed-app.wasmWIT 정의에서 각 언어의 바인딩 코드를 자동 생성하는 도구들이 있습니다.
| 언어 | 도구 | 설명 |
|---|---|---|
| Rust | cargo-component | Cargo 통합, 빌드 시 자동 생성 |
| Python | componentize-py | Python 코드를 Wasm 컴포넌트로 |
| Go | wit-bindgen-go | TinyGo 기반 바인딩 |
| JavaScript | jco | JS 컴포넌트 트랜스파일러 |
| C/C++ | wit-bindgen-c | C 헤더 파일 생성 |
// cargo-component가 자동 생성하는 바인딩 예시
// 개발자는 아래 trait만 구현하면 됩니다
impl Guest for Component {
fn get_post(id: u64) -> Option<Post> {
// 비즈니스 로직 구현
Some(Post {
id,
title: "Hello".to_string(),
content: "World".to_string(),
author: "Kreath".to_string(),
published: true,
tags: vec!["wasm".to_string()],
created_at: 1234567890,
})
}
fn list_posts(page: u32, per_page: u32, order: SortOrder) -> Vec<Post> {
// 구현
vec![]
}
}이번 장에서는 컴포넌트 모델과 WIT를 상세히 살펴보았습니다.
5장에서는 이론을 실천으로 옮깁니다. Rust에서 Wasm을 빌드하는 전체 과정을 다루며, wasm-pack과 cargo-component 사용법, 바이너리 크기 최적화, 컴포넌트 모델 적용, 그리고 HTTP 핸들러 실전 예제를 작성합니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
Rust에서 WebAssembly를 빌드하는 전체 과정을 다룹니다. wasm-pack, cargo-component, 크기 최적화, WASI 타겟 빌드, 컴포넌트 모델 적용, HTTP 핸들러 실전 예제까지.
WASI의 탄생 배경과 Capability-based 보안 모델, WASI Preview 2의 Worlds 개념, 비동기 지원을 위한 WASI 0.3, 그리고 1.0 로드맵을 상세히 다룹니다.
TinyGo, Python(componentize-py), C/C++(Emscripten), AssemblyScript, .NET Blazor 등 다양한 언어의 Wasm 지원 현황과 제약 사항, 언어 선택 가이드를 다룹니다.