WebAssembly 기초
WebAssembly의 바이너리 실행 모델과 JavaScript 연동을 이해하고 C 함수를 Wasm으로 빌드해 브라우저에서 호출합니다.
자바스크립트는 동적인 웹 페이지를 만드는 데 유연하고 쓰기 좋은 언어입니다. 다만 3D 게임, 동영상 편집, 이미지 처리, 과학 계산처럼 CPU를 많이 쓰는 작업에서는 한계가 드러나기도 합니다. 동적 타입과 실행 중의 JIT 최적화, 가비지 컬렉션, 메인 스레드에서 화면을 함께 처리하는 구조가 맞물려, 무거운 반복 계산에서 예측할 수 있는 성능을 내기 어려울 수 있기 때문입니다.
이 문제를 보완하려고 나온 기술이 WebAssembly(Wasm)입니다. 이 글에서는 WebAssembly가 무엇이고 왜 필요한지, 어떻게 동작하는지 정리하고, C 함수를 WebAssembly로 빌드해 자바스크립트에서 호출하는 흐름을 따라가 봅니다. 앞 글 Progressive Web Apps (PWA) 소개에서 자원을 비동기로 불러오고 배포하는 흐름을 봤다면, 이번에는 코드를 빌드하는 단계와 브라우저에서 모듈을 초기화해 호출하는 단계를 구별합니다. 예제의 작은 덧셈은 호출 방법을 익히기 위한 것이며 성능을 비교하는 자료가 아닙니다.
WebAssembly (Wasm)이란?
WebAssembly는 웹 브라우저와 여러 런타임에서 실행되도록 설계된, 작고 효율적인 이진 명령 형식이자 저수준 컴파일 대상입니다. 사람이 직접 쓰는 언어라기보다, C, C++, Rust 같은 언어로 쓴 코드를 컴파일해 웹에서 실행하기 위한 형식에 가깝고, 자바스크립트와 함께 동작하도록 설계되었습니다.
WebAssembly 모듈은 텍스트가 아닌 작은 이진 형식으로 배포되므로 전송하고 해석하기에 유리합니다. 브라우저의 WebAssembly 엔진이 모듈을 검증한 뒤 격리된 샌드박스(sandbox) 안에서 실행하며, 자바스크립트가 WebAssembly 모듈의 함수를 호출하고, WebAssembly도 자바스크립트가 넘겨준 함수를 쓸 수 있습니다.
WebAssembly가 필요한 이유
WebAssembly는 CPU를 많이 쓰는 계산을 네이티브에 가까운 성능으로 실행하도록 설계되었습니다. 모든 자바스크립트 코드보다 항상 빠른 것은 아니지만, 게임, 이미지와 동영상 편집, CAD, VR과 AR, 과학 시뮬레이션처럼 계산량이 큰 작업에 유리합니다. 또 C, C++, Rust로 이미 만들어 둔 고성능 라이브러리나 애플리케이션을 웹으로 옮겨 다시 쓸 수 있습니다. 웹 워커와 함께 쓰면 무거운 계산을 메인 스레드 밖으로 옮겨 화면의 반응성도 지킬 수 있습니다.
다만 성능은 자바스크립트와 WebAssembly 모두 엔진과 컴파일, 메모리, 입력의 크기에 따라 달라집니다. 처음 불러오는 비용과 반복해서 호출하는 비용, 자바스크립트와 데이터를 주고받는 비용까지 포함해 같은 작업을 비교해야 실제로 이득이 있는지 판단할 수 있습니다.
WebAssembly와 자바스크립트
| 구분 | WebAssembly | 자바스크립트 |
|---|---|---|
| 타입 | 정적 타입 (컴파일할 때 정해짐) | 동적 타입 (실행할 때 정해짐) |
| 실행 속도 | 계산이 많은 작업에서 예측할 수 있는 성능에 유리 | 일반 웹 로직에 최적화, 무거운 계산은 부담이 될 수 있음 |
| 형식 | 작은 이진 형식, 전송과 해석에 유리 | 텍스트 형식, 해석과 최적화 과정이 필요 |
| 개발 언어 | C, C++, Rust 등 여러 언어의 컴파일 대상 | 자바스크립트 자체 |
| 주로 쓰는 곳 | 고성능 계산, 게임, 이미지 처리, 동영상 인코딩 | DOM 조작, 화면 상호작용, 일반 웹 로직 |
| 함께 쓰기 | 자바스크립트와 서로 호출 가능 | WebAssembly 호출 가능 |
WebAssembly는 자바스크립트를 대신하는 기술이 아니라, 자바스크립트가 잘하지 못하는 고성능 계산을 보완하는 동반자입니다. 대부분의 웹 애플리케이션에서는 여전히 자바스크립트가 화면과 사용자 상호작용을 맡고, WebAssembly는 필요한 계산 모듈을 맡습니다. 웹 워커는 작업을 어디서 실행할지를 나누는 수단이고 WebAssembly는 실행하는 코드의 형식이라서, 둘은 하나를 고르는 대안이 아니라 함께 쓸 수 있는 선택입니다. DOM과 사용자 입력을 연결하는 자바스크립트 코드, 자바스크립트와 WebAssembly 사이의 값 전달, 오류가 났을 때의 대체 경로는 여전히 만들어야 합니다.
WASM FIT
Wasm의 이득은 계산 시간과 호출·로딩 비용을 함께 비교합니다
대표 입력으로 병목을 측정한 뒤 알고리즘, 실행 위치, 실행 형식을 검토합니다. Worker는 UI와 계산을 분리하고, Wasm은 계산 모듈의 실행 형식을 바꾸므로 두 선택을 함께 사용할 수도 있습니다.
먼저 실제 병목을 측정
CPU 계산, 네트워크 대기, DOM 갱신 중 시간이 어디에 쓰이는지 확인합니다. 계산이 병목이 아니면 Wasm 도입만으로 해당 문제가 해결되기 어렵습니다.
JavaScript · 일반 웹 로직
DOM·UI·요청 처리와 일반 애플리케이션 로직에 사용합니다. 먼저 알고리즘·데이터 구조·불필요한 작업을 개선합니다.
비교: 대표 입력의 처리 시간과 사용자 반응성.
Worker · 실행 위치 분리
긴 계산이 메인 스레드를 막는다면 Worker를 검토합니다. 작업 전달·결과 수신에도 비용이 있고, 이동 자체가 계산량을 줄이지는 않습니다.
비교: UI 멈춤, 계산 완료 시간, 메시지 전달 비용.
Wasm · 계산 모듈
계산 집약적 루프나 기존 C/C++/Rust 모듈을 검토합니다. Wasm을 Worker 안에서 실행할 수도 있습니다.
비교: 계산 시간 + 로드·초기화 + JS 호출·데이터 변환.
모듈 경계를 정하기
자잘한 호출을 많이 하는 대신 큰 작업 단위로 교환할 수 있는지 확인합니다. 버퍼 소유·복사·반환 형식과 디버깅·배포 비용도 함께 평가합니다.
같은 조건으로 최종 비교
JS와 Wasm을 같은 대표 입력·기기에서 비교합니다. 처음 실행과 반복 실행을 구분하고, 실제 이득이 유지될 때 도입합니다. 로드 실패나 미지원에 대한 대체 경로도 준비합니다.
위 그림은 WebAssembly를 쓸지 판단하는 방법입니다. 먼저 대표적인 입력으로 실제 병목을 측정해, 시간이 CPU 계산과 네트워크 대기, DOM 갱신 가운데 어디에 쓰이는지 확인합니다. 계산이 병목이 아니라면 WebAssembly를 들여도 그 문제는 풀리기 어렵습니다.
그다음 세 가지를 따로 검토합니다. 알고리즘과 입력의 양이 문제라면 먼저 자바스크립트의 알고리즘과 데이터 구조, 불필요한 작업을 개선해 같은 작업으로 비교합니다. 긴 계산이 메인 스레드를 막아 화면이 멈춘다면 실행 위치를 웹 워커로 옮기는데, 작업을 보내고 결과를 받는 비용도 있고 옮긴다고 계산량이 줄지는 않습니다. 계산이 많은 반복문이나 기존의 C, C++, Rust 모듈이 있다면 실행 형식을 WebAssembly로 바꾸는 것을 검토하며, 계산 시간과 함께 불러오기와 초기화, 자바스크립트 호출과 데이터 변환의 비용을 처음 실행과 반복 실행으로 나눠 비교합니다.
세 선택은 서로 배타적이지 않아서, 웹 워커 안에서 WebAssembly를 실행할 수도 있습니다. 그림 아래쪽처럼 모듈을 나눌 때는 자잘한 호출을 많이 하기보다 큰 작업 단위로 주고받을 수 있는지, 버퍼를 누가 갖고 복사하고 돌려주는지, 디버깅과 배포 비용은 어떤지 함께 평가합니다. 마지막으로 자바스크립트와 WebAssembly를 같은 입력과 기기에서 비교해 실제 이득이 유지될 때 들이고, 불러오기에 실패하거나 지원하지 않는 환경을 위한 대체 경로도 준비합니다.
계산으로 비교해 보기
실제 측정 대신, 비교를 연습하기 위한 가정을 세워 봅니다. 데이터 한 묶음을 처리하는 데 자바스크립트는 12ms가 걸리고, WebAssembly는 처음 한 번의 초기화에 18ms, 묶음마다 계산 4ms와 데이터 전달 3ms가 걸린다고 가정합니다. 첫 묶음은 WebAssembly가 18 + 4 + 3 = 25ms라서 12ms보다 깁니다. 세 묶음이면 18 + 3 × 7 = 39ms 대 36ms로 여전히 자바스크립트가 빠르고, 네 묶음이 되어야 18 + 4 × 7 = 46ms 대 48ms로 WebAssembly가 앞섭니다.
반복 계산이 빠르다는 점만 보면 첫 호출의 비용을 놓칩니다. 이 수치는 계산을 위한 가정일 뿐 실제 브라우저나 툴체인의 성능이 아닙니다.
WebAssembly의 동작 원리
WebAssembly가 웹 브라우저에서 실행되는 흐름은 다음과 같습니다. 먼저 개발자가 이미지 필터 같은 고성능 로직을 C, C++, Rust 같은 언어로 작성합니다. 그다음 C와 C++는 Emscripten, Rust는 wasm-pack 같은 툴체인으로 컴파일해 .wasm 이진 파일과, 그 파일을 불러와 실행하는 자바스크립트 연결 코드(glue code)를 만듭니다.
웹 페이지는 이 .wasm 파일과 자바스크립트 파일을 내려받습니다. 실제 크기는 모듈과 런타임을 얼마나 포함했는지에 따라 다르지만, 이진 형식은 전송과 해석에 유리하게 설계되어 있습니다. 브라우저의 WebAssembly 엔진은 타입과 구조가 분명한 이진 코드를 내려받는 동안 검증하고 컴파일할 수 있습니다. 컴파일된 코드는 샌드박스 안에서 실행되며, 자바스크립트가 필요할 때 WebAssembly 모듈의 함수를 호출하고 결과를 받습니다.
WebAssembly 사용 예시
WebAssembly 빌드·호출 데이터 흐름 — 상태 추적
본문 add.c→emcc 산출물→await factory→_add(10,20)→30에서 생성 파일과 호출 준비 상태가 달라진다. 다운로드와 인스턴스 준비/호출 성공을 구분한다.
EMSCRIPTEN EXAMPLE
add.c를 빌드한 JS factory를 기다린 뒤 _add를 호출합니다
이 절은 Emscripten의 MODULARIZE·EXPORT_ES6 경로를 사용합니다. add.js와 add.wasm을 함께 제공하고, JavaScript wrapper가 로드·초기화를 마친 뒤 함수를 호출합니다.
빌드 산출물 · C → JS와 Wasm
add.c → emcc
add.js · add.wasmadd.js는 ES module의 default factory를 내보냅니다. HTML과 index.js는 따로 준비하며, 이 예제는 생성 파일들을 같은 경로에서 HTTP로 제공합니다.
브라우저에서 초기화 후 호출
const module =
await createAddModule();
module._add(10, 20); // 30초기화 Promise가 완료된 뒤 JS wrapper의 _add를 호출합니다. 로드·초기화·실행 실패는 예제의 catch에서 처리합니다.
raw WebAssembly 경로와 구별
WebAssembly.instantiate를 직접 사용하는 경우의 imports·exports 이름은 실제 모듈 계약을 따라야 합니다. 이 절의 module._add는 생성된 Emscripten wrapper의 이름입니다.
작은 덧셈 예제로 성능을 단정하지 않기
30은 두 정수 덧셈의 성공 시 예상값입니다. 이 흐름은 API 사용법을 보여주며, 툴체인 빌드나 성능 측정을 수행한 결과는 아닙니다. 큰 데이터의 메모리 전달은 별도 인터페이스를 설계합니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 도표 이름 | 산출물 생성과 초기화 뒤 호출을 구별한다 |
| 전체 관계·조건 설명 | 노드 6개·순서 관계 5개다. 첫 영역은 C 소스→emcc→JS/Wasm 산출물이며 둘째 영역은 브라우저 await factory→wrapper _add 호출→성공 시 30이다. 실패 시 catch이며 다운로드만으로 호출 성공·성능 우위를 보증하지 않는다. 원 760 SVG 전체 bytes는 지원용 숨김 자료로 보존하고 new mobile은 동일한 두 영역과 순서를 따른다. |
| 원도표 설명 | 30은 두 정수 덧셈의 성공 시 예상값입니다. 이 흐름은 API 사용법을 보여 주며, 툴체인 빌드나 성능 측정을 수행한 결과는 아닙니다. 큰 데이터의 메모리 전달은 별도 인터페이스를 설계합니다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| C 소스 빌드 | 입력: add.c / 산출: JS glue + wasm | emcc가 add.js와 wasm을 준비한다 | 브라우저가 C 소스를 직접 실행하지 않는다 |
| factory 준비 | 모듈: 준비 중→사용 가능 / 호출: 아직 실행 안 함 | 모듈 factory의 완료를 await한다 | 비동기 초기화 뒤에 export를 사용한다 |
| add 실행 | 인수: 10,20 / 반환: 30 | _add(10,20)을 호출한다 | 결과 계약을 확인하며 속도 우위를 추정하지 않는다 |
WebAssembly 빌드·호출 데이터 흐름 — 상태 추적
본문 add.c→emcc 산출물→await factory→_add(10,20)→30에서 생성 파일과 호출 준비 상태가 달라진다. 다운로드와 인스턴스 준비/호출 성공을 구분한다.
EMSCRIPTEN EXAMPLE
add.c를 빌드한 JS factory를 기다린 뒤 _add를 호출합니다
이 절은 Emscripten의 MODULARIZE·EXPORT_ES6 경로를 사용합니다. add.js와 add.wasm을 함께 제공하고, JavaScript wrapper가 로드·초기화를 마친 뒤 함수를 호출합니다.
빌드 산출물 · C → JS와 Wasm
add.c → emcc
add.js · add.wasmadd.js는 ES module의 default factory를 내보냅니다. HTML과 index.js는 따로 준비하며, 이 예제는 생성 파일들을 같은 경로에서 HTTP로 제공합니다.
브라우저에서 초기화 후 호출
const module =
await createAddModule();
module._add(10, 20); // 30초기화 Promise가 완료된 뒤 JS wrapper의 _add를 호출합니다. 로드·초기화·실행 실패는 예제의 catch에서 처리합니다.
raw WebAssembly 경로와 구별
WebAssembly.instantiate를 직접 사용하는 경우의 imports·exports 이름은 실제 모듈 계약을 따라야 합니다. 이 절의 module._add는 생성된 Emscripten wrapper의 이름입니다.
작은 덧셈 예제로 성능을 단정하지 않기
30은 두 정수 덧셈의 성공 시 예상값입니다. 이 흐름은 API 사용법을 보여주며, 툴체인 빌드나 성능 측정을 수행한 결과는 아닙니다. 큰 데이터의 메모리 전달은 별도 인터페이스를 설계합니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 도표 이름 | 산출물 생성과 초기화 뒤 호출을 구별한다 |
| 전체 관계·조건 설명 | 노드 6개·순서 관계 5개다. 첫 영역은 C 소스→emcc→JS/Wasm 산출물이며 둘째 영역은 브라우저 await factory→wrapper _add 호출→성공 시 30이다. 실패 시 catch이며 다운로드만으로 호출 성공·성능 우위를 보증하지 않는다. 원 760 SVG 전체 bytes는 지원용 숨김 자료로 보존하고 new mobile은 동일한 두 영역과 순서를 따른다. |
| 원도표 설명 | 30은 두 정수 덧셈의 성공 시 예상값입니다. 이 흐름은 API 사용법을 보여 주며, 툴체인 빌드나 성능 측정을 수행한 결과는 아닙니다. 큰 데이터의 메모리 전달은 별도 인터페이스를 설계합니다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| C 소스 빌드 | 입력: add.c / 산출: JS glue + wasm | emcc가 add.js와 wasm을 준비한다 | 브라우저가 C 소스를 직접 실행하지 않는다 |
| factory 준비 | 모듈: 준비 중→사용 가능 / 호출: 아직 실행 안 함 | 모듈 factory의 완료를 await한다 | 비동기 초기화 뒤에 export를 사용한다 |
| add 실행 | 인수: 10,20 / 반환: 30 | _add(10,20)을 호출한다 | 결과 계약을 확인하며 속도 우위를 추정하지 않는다 |
위 그림은 이 절의 예제가 거치는 흐름을 빌드 시점과 브라우저 실행으로 나눠 보여 줍니다. 빌드 시점에는 C 소스 add.c를 Emscripten의 emcc로 빌드하면서 _add 함수를 내보내도록 지정하고, 그 결과로 add.js와 add.wasm이 만들어집니다. 두 파일은 같은 경로에서 HTTP로 함께 제공합니다.
브라우저에서는 add.js가 내보내는 팩토리 함수를 호출하고, 모듈의 불러오기와 초기화가 끝나기를 await로 기다립니다. 초기화가 끝나면 자바스크립트 연결 코드의 module._add(10, 20)을 호출하고, 성공하면 30을 얻습니다. 불러오기나 초기화, 실행에 실패하면 예제의 catch에서 처리합니다.
그림 아래쪽의 설명은 두 가지를 구별합니다. WebAssembly.instantiate()로 모듈을 직접 다룰 때의 가져오기(imports)와 내보내기(exports) 이름은 그 모듈이 정한 대로 따라야 하며, 이 절의 module._add는 Emscripten이 만든 연결 코드의 이름입니다. 또 30은 두 정수를 더한 결과가 맞다는 확인일 뿐, 툴체인을 실행하거나 성능을 측정한 결과가 아닙니다. 큰 데이터를 메모리로 주고받는 일은 따로 설계해야 합니다.
먼저 C로 간단한 덧셈 함수를 작성합니다.
// add.c
int add(int a, int b) {
return a + b;
}Emscripten 툴체인으로 WebAssembly를 만듭니다. Emscripten의 설치 과정은 이 글에서 다루지 않으므로 명령만 보여 줍니다. 직접 따라 하려면 Emscripten의 설치 안내대로 emsdk를 설치하고 활성화한 뒤, 환경 변수를 적용한 터미널에서 아래 명령을 실행합니다.
# main 없이 add 함수를 내보내는 JS factory와 add.wasm 생성
emcc add.c -o add.js --no-entry -s MODULARIZE=1 -s EXPORT_ES6=1 -s EXPORTED_FUNCTIONS="['_add']"--no-entry는 main 함수가 없는 라이브러리 형태로 만든다는 뜻이고, MODULARIZE=1과 EXPORT_ES6=1은 모듈을 초기화하는 팩토리 함수를 ES 모듈의 기본 내보내기로 만들며, EXPORTED_FUNCTIONS는 바깥에서 부를 함수 _add를 지정합니다. C 함수 이름 앞에는 밑줄이 붙습니다. 이 명령은 add.js와 add.wasm을 만들며, HTML은 따로 준비합니다.
자바스크립트에서 모듈을 불러와 함수를 호출합니다. 이 파일은 HTML에서 <script type="module" src="./index.js"></script>로 불러오고, add.js, add.wasm과 같은 경로에서 HTTP로 제공합니다.
// <script type="module" src="./index.js"></script>로 로드
import createAddModule from './add.js';
async function loadWasmModule() {
try {
// 생성된 JS factory가 Wasm 로드·초기화와 필요한 imports를 처리
const module = await createAddModule();
const addFunction = module._add; // Emscripten JS wrapper의 내보내기 이름
const result = addFunction(10, 20);
console.log(`WebAssembly를 사용한 덧셈 결과: ${result}`); // 성공 시 예상값: 30
} catch (error) {
console.error('WebAssembly 모듈 로드 또는 실행 실패:', error);
}
}
loadWasmModule();createAddModule()은 모듈 인스턴스가 준비되면 이행되는 Promise를 돌려주므로, await가 끝나기 전에 _add를 호출하면 안 됩니다(Emscripten의 모듈화 출력 문서). 초기화가 거부되면 catch로 가고, 초기화가 끝났는데 _add가 함수가 아니라면 호출하는 줄에서 오류가 납니다. 실제 서비스에서도 Emscripten이 만든 연결 코드를 쓰면, .wasm 파일을 불러오고 필요한 가져오기를 연결하는 일을 대신 해 주어 편리합니다.
WebAssembly의 활용 분야와 앞으로
WebAssembly는 이미 여러 분야에서 쓰입니다. 웹 기반 게임과 게임 엔진의 웹 출력, 오래된 게임기 에뮬레이터, 브라우저 안의 실시간 이미지 필터와 압축, 편집 도구, 웹 기반 3D 뷰어와 설계 도구, 복잡한 수치 계산과 머신러닝 모델의 추론이 대표적입니다. 여기서 든 제품이나 분야는 쓰임을 이해하기 위한 예일 뿐이며, 실제로 적용할 때는 고른 도구와 버전의 공식 문서를 확인합니다.
활용 범위도 브라우저 밖으로 넓어지고 있습니다. WASI(WebAssembly System Interface)는 서버나 IoT 기기처럼 브라우저 밖의 환경에서도 WebAssembly를 실행하게 해 주는 인터페이스로, WebAssembly를 범용 런타임으로 넓힙니다. 가비지 컬렉션(GC) 지원은 Java, Kotlin, Scala, OCaml처럼 GC에 기대는 언어를 WebAssembly로 옮기기 쉽게 하고, 공유 메모리와 웹 워커를 이용한 멀티스레딩은 병렬 처리의 성능을 높이며, 개발자 도구에서의 디버깅 경험도 계속 나아지고 있습니다.
WebAssembly는 계산량이 큰 기능을 웹에서 처리해야 할 때 검토할 수 있는 선택지입니다. 적용하기 전에 빠른 실행만 보지 말고, 자바스크립트와 주고받는 비용, 메모리 전달, 빌드의 복잡도, 대체 경로를 함께 따져야 합니다.
더 깊이: WebAssembly 3.0에서 달라진 것
WebAssembly 표준은 2025년 9월 17일에 3.0판이 확정되었고, 발표 글에 따르면 대부분의 주요 브라우저가 이미 이 기능들을 지원하고 있습니다. 가장 눈에 띄는 변화는 가비지 컬렉션으로, 모듈이 실행 환경이 회수를 맡는 객체를 만들어 쓸 수 있게 되었습니다. 발표 글은 Java, Kotlin, Scala, OCaml, Scheme, Dart가 이 기능을 써서 WebAssembly를 대상으로 삼고 있다고 소개합니다.
메모리 모델도 넓어졌습니다. 주소를 32비트 정수로 다루던 예전에는 한 메모리가 4GB를 넘을 수 없었지만, 3.0의 64비트 주소 공간(memory64)으로 그보다 큰 메모리를 쓸 수 있고, 한 모듈이 여러 개의 메모리를 가질 수도 있습니다. 이 밖에도 꼬리 호출(tail call), 언어 차원의 예외 처리, 형식이 있는 참조와 이를 이용한 안전한 간접 호출, 자바스크립트 문자열을 WebAssembly에서 바로 다루는 내장 함수가 들어왔습니다.
그래서 메모리를 직접 다루는 C나 Rust뿐 아니라, 가비지 컬렉션에 기대는 언어도 WebAssembly를 컴파일 대상으로 삼을 수 있게 되었습니다. 다만 브라우저마다 버전이 다를 수 있으므로, 이 기능을 쓰는 모듈을 배포하기 전에는 대상 브라우저에서 동작하는지 확인합니다. 기능 목록은 WebAssembly 3.0 발표 글에 있습니다.
정리
- WebAssembly는 C, C++, Rust 같은 언어의 컴파일 대상인 작은 이진 실행 형식으로, 브라우저가 검증한 뒤 샌드박스에서 실행합니다.
- WebAssembly는 자바스크립트를 대신하지 않고, 자바스크립트가 화면과 상호작용을, WebAssembly가 고성능 계산을 맡는 식으로 함께 씁니다.
- 도입하기 전에 실제 병목을 측정하고, 알고리즘 개선, 웹 워커로 실행 위치 옮기기, WebAssembly로 실행 형식 바꾸기를 따로 검토합니다.
- 성능은 계산 시간뿐 아니라 불러오기와 초기화, 자바스크립트 호출과 데이터 전달 비용까지 처음 실행과 반복 실행으로 나눠 비교합니다.
- Emscripten의 모듈화 출력은 초기화가 끝나면 이행되는 팩토리를 돌려주므로,
await로 기다린 뒤_add같은 함수를 호출합니다. - 내려받기, 초기화, 함수 호출, 결과 확인은 서로 다른 단계이며, 작은 예제의 결과로 성능을 판단하지 않습니다.
연습 문제
1. 묶음 수에 따른 비교
본문의 가정과 같은 입력, 같은 재사용 조건에서 두 묶음과 다섯 묶음을 처리하는 총시간을 각각 구해 보세요. 묶음마다 모듈을 새로 초기화한다면 결론이 같을까요? 작은 _add의 결과가 30이라는 사실로 WebAssembly가 더 빠르다고 판단할 수 있을까요?
해설 보기
두 묶음은 자바스크립트 24ms, WebAssembly 18 + 2 × 7 = 32ms입니다. 다섯 묶음은 자바스크립트 60ms, WebAssembly 18 + 5 × 7 = 53ms입니다.
묶음마다 새로 초기화하면 WebAssembly는 묶음마다 25ms가 걸려, 이 조건에서는 이득이 없습니다. 30은 덧셈 결과가 맞다는 확인일 뿐, 실행 시간이나 주고받는 비용을 비교한 결과가 아닙니다.
2. 초기화가 실패하면
예제에서 add.wasm 파일을 찾지 못해 팩토리의 Promise가 거부되었습니다. addFunction(10, 20) 호출까지 도달할까요? 반대로 팩토리는 끝났는데 _add가 없다면 무엇을 확인해야 할까요? 두 파일을 내려받은 것만으로 호출이 성공했다고 볼 수 있을까요?
해설 보기
도달하지 않습니다. 초기화가 거부되면 await에서 바로 catch로 넘어가므로 덧셈 호출은 실행되지 않습니다.
_add가 없다면 빌드 명령의 EXPORTED_FUNCTIONS에 _add를 넣었는지와, 실제로 만들어진 연결 코드와 .wasm 파일이 같은 빌드의 짝인지 확인합니다. 내려받기, 초기화, 함수 호출, 결과 확인은 서로 다른 단계라서, 파일을 받았다는 사실만으로 30을 얻었다고 할 수는 없습니다.
3. 화면이 멈추는 계산
버튼을 누르면 자바스크립트가 200ms 동안 계산을 해서 그동안 화면이 멈춥니다. 이 문제를 WebAssembly로 바꾸기만 하면 화면이 멈추지 않게 될까요? 어떤 선택지를 검토해야 할까요?
해설 보기
꼭 그렇지는 않습니다. WebAssembly로 바꿔 계산 시간이 줄어도, 메인 스레드에서 실행하는 한 그 시간만큼은 여전히 화면이 멈춥니다. 화면이 멈추는 문제는 실행 위치의 문제입니다.
먼저 알고리즘을 개선할 수 있는지 보고, 계산을 웹 워커로 옮겨 메인 스레드를 비우는 것을 검토합니다. 계산 자체도 줄여야 한다면 웹 워커 안에서 WebAssembly를 실행하는 방법을 함께 쓸 수 있으며, 어느 경우든 같은 조건에서 측정해 비교합니다.
다음 글 JAMstack 아키텍처 이해에서는 미리 만든 정적 파일과 API, 자바스크립트를 조합해 웹 사이트를 만드는 JAMstack의 빌드와 배포, 요청 흐름을 살펴봅니다.