C++ WebAssembly(Wasm)와 Emscripten | C++을 브라우저에서 돌리기
들어가며: C++ 코드를 브라우저에서 돌리고 싶습니다
프론트엔드까지 닿는 C++
WebAssembly(Wasm)(웹어셈블리—브라우저에서 네이티브에 가까운 속도로 실행되는 저수준 바이너리 포맷. JS보다 빠른 연산이 필요할 때 사용)는 브라우저가 네이티브에 가까운 속도로 실행할 수 있는 바이너리 포맷입니다. C/C++를 Wasm으로 컴파일하면, 서버 없이 브라우저 안에서 기존 C++ 라이브러리나 게임 엔진을 돌릴 수 있습니다.
Emscripten은 LLVM 기반으로 C/C++ 소스를 Wasm(또는 asm.js)과 JavaScript 글루 코드(JS와 Wasm을 이어 주는 코드)로 바꿔 주는 툴체인(컴파일·링크 등 빌드 도구 묶음)입니다. 메인스트림 브라우저가 Wasm을 지원한 이후, “C++로 작성한 로직을 웹에 그대로 가져오기” 수요가 커졌으며, 프론트엔드 개발자들도 검색·도입을 많이 합니다.
추가 문제 시나리오
이미지 처리 병목
웹 앱에서 4K 이미지 리사이즈·필터 적용 시 JavaScript Canvas API만으로는 수 초가 걸립니다. C++ OpenCV나 libvips를 Wasm으로 컴파일하면 수백 ms 수준으로 단축됩니다.
게임 엔진 포팅
기존 C++ 게임을 웹에서 실행해야 할 때, Unity·Unreal처럼 별도 런타임 대신 핵심 로직만 Wasm으로 빌드해 번들 크기를 줄일 수 있습니다.
암호화 라이브러리 활용
OpenSSL, libsodium 같은 검증된 C 라이브러리를 브라우저에서 직접 실행해, 민감한 데이터를 서버에 보내기 전에 클라이언트에서 암호화할 수 있습니다.
레거시 C++ 도구 웹화
오프라인에서만 돌리던 C++ 분석 도구·시뮬레이터를 웹 앱으로 옮기고 싶을 때, Emscripten으로 핵심만 Wasm으로 빌드해 브라우저에서 실행할 수 있습니다.
Emscripten으로 해결
flowchart LR
subgraph before[문제 상황]
P1[JavaScript만] --> P2[느린 연산]
P2 --> P3[서버 의존]
end
subgraph after[Emscripten 적용]
A1[C++ 코드] --> A2[emcc 컴파일]
A2 --> A3[Wasm + JS]
A3 --> A4[브라우저 네이티브 속도]
end
C++ 파일 하나를 Emscripten으로 Wasm + JS로 컴파일하는 최소 빌드부터, 브라우저에서 모듈을 불러 함수를 호출하는 방법, JS 인터롭과 메모리 관리, 기존 C++ 프로젝트를 웹으로 옮길 때 부딪히는 제약까지 다룹니다.
WebAssembly와 Emscripten이란
WebAssembly (Wasm)
- 스택 기반 바이너리 포맷으로, 브라우저의 JS 엔진이 네이티브에 가까운 속도로 실행할 수 있습니다.
- C/C++/Rust 등에서 컴파일 타겟으로 사용되며, 메모리 모델이 제한적이라 (선형 메모리, 샌드박스) 보안·이식성 측면에서 브라우저에 잘 맞습니다.
- JavaScript와 상호 운용이 가능해, JS에서 Wasm 모듈을 로드하고 함수를 호출할 수 있습니다.
Emscripten
- LLVM/Clang 기반으로 C/C++를 Wasm(또는 asm.js)으로 컴파일합니다.
- 표준 C/C++ 라이브러리(libc, C++ STL 일부)를 JS/Wasm으로 구현해 링크해 주므로, 파일 I/O·malloc 등도 브라우저 환경에 맞게 동작하게 할 수 있습니다.
- emcc 라는 드라이버로
g++처럼 사용하며, 출력은 .wasm 파일과 이를 로드하는 .js 글루 코드입니다.
아키텍처 개요
flowchart TB
subgraph browser[브라우저]
HTML[HTML 페이지]
JS[JavaScript]
Module[Module 객체]
end
subgraph wasm[WebAssembly]
WASM[hello.wasm]
HEAP[선형 메모리 HEAP]
end
subgraph cpp[C++ 소스]
SRC[hello.cpp]
end
SRC -->|emcc| WASM
HTML --> JS
JS -->|로드| Module
Module --> WASM
Module --> HEAP
JS -->|"Module._add()"| WASM
왜 쓰는가
- 기존 C++ 코드를 웹에 이식할 때 (게임, 시뮬레이션, 코덱, 암호 라이브러리 등).
- 무거운 연산을 브라우저에서 돌리고 싶을 때 (이미지 처리, 물리 연산, 번역 엔진 등).
- 한 코드베이스로 데스크톱·웹을 동시에 타겟할 때.
Emscripten 설치와 최소 빌드
설치
- 공식 사이트에서 Emscripten SDK (emsdk) 를 받아 설치합니다.
emsdk install latest,emsdk activate latest후, 터미널에서source emsdk_env.sh로 환경을 활성화하면 emcc가 PATH에 올라갑니다.
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh
emcc --version # 설치 확인
최소 예제
hello.cpp: extern “C” 로 C 링크를 주면 C++ 이름 맹글링이 없어져서 JS에서 Module._add 같은 이름으로 호출하기 쉽습니다. EMSCRIPTEN_KEEPALIVE 는 “이 함수를 사용하지 않는 것으로 보고 제거하지 말고, export 목록에 넣어라”는 지시라서, add 가 Wasm 모듈에서 내보내져 JS에서 호출할 수 있게 됩니다.
// hello.cpp
#include <emscripten.h>
#include <iostream>
extern "C" {
EMSCRIPTEN_KEEPALIVE
int add(int a, int b) {
return a + b;
}
}
- EMSCRIPTEN_KEEPALIVE: 이 함수가 “사용되지 않음”으로 제거되지 않도록 하며, export 되게 합니다.
- extern “C”: C 링크로 내보내면 JS에서 이름으로 호출하기 쉽습니다. 빌드:
emcc hello.cpp -o hello.js
- hello.js: Wasm을 로드하고 인스턴스화하는 글루 코드.
- hello.wasm: 실제 바이너리 (hello.js가 로드함). Node에서 간단 확인:
// node_hello.js
const Module = require('./hello.js');
Module.onRuntimeInitialized = () => {
console.log(Module._add(1, 2)); // 3
};
node node_hello.js
브라우저에서는 hello.js를 스크립트로 넣으며, 비동기 로드가 끝난 뒤 Module._add(1, 2) 처럼 호출할 수 있습니다 (빌드 옵션에 따라 Module.cwrap 등으로 감싸서 씀).
HTML 출력으로 한 번에
emcc hello.cpp -o hello.html
- hello.html이 생성되고, 스크립트 로드·Wasm 초기화가 포함됩니다. 로컬 웹 서버로 열어서 동작을 확인할 수 있습니다.
브라우저에서 Wasm 호출하기
로드 순서
- Wasm은 비동기로 로드되는 경우가 많으므로, Promise 기반 또는 onRuntimeInitialized 콜백을 사용합니다.
- Module 객체가 준비된 뒤, Module._add 같은 export된 C 함수를 호출합니다.
- 메모리: C++ 쪽에서 쓰는 선형 메모리는 Module.HEAP8/HEAP32 등으로 JS에서 접근할 수 있어, 배열·버퍼를 공유할 때 사용합니다.
시퀀스 다이어그램
sequenceDiagram participant HTML as HTML participant JS as JavaScript participant Module as Module participant WASM as Wasm 모듈 HTML->>JS: 스크립트 로드 JS->>Module: Module 로드 Module->>WASM: Wasm 바이너리 fetch WASM-->>Module: 인스턴스화 완료 Module->>JS: onRuntimeInitialized 콜백 JS->>Module: Module._add(1, 2) Module->>WASM: 함수 호출 WASM-->>Module: 3 반환 Module-->>JS: 결과 반환
예시 (개념)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Wasm Hello</title>
</head>
<body>
<script src="hello.js"></script>
<script>
Module.onRuntimeInitialized = function() {
console.log(Module._add(1, 2)); // 3
};
</script>
</body>
</html>
- export 되는 이름은
extern "C"와 빌드 옵션(EXPORTED_FUNCTIONS, EXPORTED_RUNTIME_METHODS 등)에 따라 달라집니다. Emscripten 문서의 “Exporting functions”를 참고하면 됩니다.
문자열 반환, 버퍼 전달, Embind 클래스 노출 예제
예제 1: C++ → Wasm 완전 빌드 (문자열 반환)
C++에서 문자열을 반환할 때는 메모리 할당과 JS에서의 해제가 중요합니다. 아래 예제는 malloc으로 할당한 버퍼를 JS에 넘기고, JS에서 Module._free()로 해제하는 패턴을 보여줍니다.
// greet.cpp
#include <emscripten.h>
#include <cstring>
#include <string>
extern "C" {
EMSCRIPTEN_KEEPALIVE
const char* greet(const char* name) {
std::string msg = "Hello, ";
msg += name;
msg += "!";
// C 스타일 문자열로 반환 (호출자가 free 해야 함)
char* result = (char*)malloc(msg.size() + 1);
strcpy(result, msg.c_str());
return result;
}
EMSCRIPTEN_KEEPALIVE
void free_string(char* ptr) {
free(ptr);
}
}
빌드:
emcc greet.cpp -o greet.js -s EXPORTED_RUNTIME_METHODS=['ccall','cwrap'] -s EXPORTED_FUNCTIONS=['_greet','_free_string','_malloc','_free']
HTML에서 호출:
<script src="greet.js"></script>
<script>
Module.onRuntimeInitialized = () => {
const greet = Module.cwrap('greet', 'string', ['string']);
const result = greet('World');
console.log(result); // "Hello, World!"
// cwrap의 string 반환은 내부적으로 복사 후 free 처리됨
};
</script>
예제 2: 배열/버퍼 전달 (이미지 처리 시뮬레이션)
JS에서 Uint8Array를 C++에 넘기고, C++에서 처리한 뒤 결과를 반환하는 예제입니다. 선형 메모리(HEAP8)를 공유합니다.
// image_process.cpp
#include <emscripten.h>
#include <cstdint>
#include <cstring>
extern "C" {
// ptr: Module.HEAP8 내 오프셋, size: 바이트 수
// 그레이스케일 변환 (간단한 예시)
EMSCRIPTEN_KEEPALIVE
void grayscale(uint8_t* ptr, int width, int height) {
const int bytesPerPixel = 4; // RGBA
const int stride = width * bytesPerPixel;
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
int idx = y * stride + x * bytesPerPixel;
uint8_t r = ptr[idx];
uint8_t g = ptr[idx + 1];
uint8_t b = ptr[idx + 2];
uint8_t gray = (r * 77 + g * 150 + b * 29) >> 8;
ptr[idx] = ptr[idx + 1] = ptr[idx + 2] = gray;
}
}
}
}
빌드:
emcc image_process.cpp -o image_process.js -s EXPORTED_FUNCTIONS=['_grayscale'] -O3
JS에서 호출:
// image_process 호출 예시
Module.onRuntimeInitialized = () => {
const width = 100, height = 100;
const size = width * height * 4; // RGBA
const ptr = Module._malloc(size);
const heap = new Uint8Array(Module.HEAP8.buffer, ptr, size);
// 원본 이미지 데이터로 heap 채우기
heap.set(imageData);
Module._grayscale(ptr, width, height);
// 결과는 heap에 그대로 반영됨
const result = new Uint8Array(heap);
Module._free(ptr);
};
예제 3: Embind로 C++ 클래스 노출 (고급)
Embind를 사용하면 C++ 클래스를 JS 객체처럼 다룰 수 있습니다.
// calculator.cpp
#include <emscripten/bind.h>
class Calculator {
public:
int add(int a, int b) const { return a + b; }
int mul(int a, int b) const { return a * b; }
};
EMSCRIPTEN_BINDINGS(calculator) {
emscripten::class_<Calculator>("Calculator")
.constructor<>()
.function("add", &Calculator::add)
.function("mul", &Calculator::mul);
}
빌드:
emcc calculator.cpp -o calculator.js -lembind -s MODULARIZE=1 -s EXPORT_NAME='createModule'
JS에서 호출:
import createModule from './calculator.js';
const Module = await createModule();
const calc = new Module.Calculator();
console.log(calc.add(2, 3)); // 5
console.log(calc.mul(2, 3)); // 6
메모리 관리와 JS 인터롭
선형 메모리 구조
Wasm은 단일 선형 메모리를 가집니다. Emscripten이 제공하는 HEAP8, HEAP16, HEAP32, HEAPF32, HEAPF64는 이 메모리의 뷰입니다.
flowchart LR
subgraph heap["선형 메모리 (Wasm)"]
H[0x0000 ... 0xFFFF]
end
subgraph views[JS 뷰]
V8[HEAP8]
V32[HEAP32]
VF32[HEAPF32]
end
H --> V8
H --> V32
H --> VF32
메모리 할당/해제 규칙
| 작업 | C++ | JavaScript |
|---|---|---|
| 할당 | malloc() | Module._malloc(size) |
| 해제 | free() | Module._free(ptr) |
| 접근 | 포인터 | new Uint8Array(Module.HEAP8.buffer, ptr, size) |
주의: C++에서 malloc으로 할당한 메모리는 반드시 Module._free(ptr)로 해제해야 합니다. JS에 포인터만 넘기고 해제하지 않으면 메모리 누수가 발생합니다.
ccall / cwrap 사용법
ccall과 cwrap은 타입 변환을 자동으로 처리해 줍니다.
// ccall: 한 번만 호출할 때
const result = Module.ccall(
'add', // 함수명
'number', // 반환 타입: number, string, null
['number', 'number'], // 인자 타입
[1, 2] // 인자 값
);
// cwrap: 여러 번 호출할 때 (함수 반환)
const add = Module.cwrap('add', 'number', ['number', 'number']);
console.log(add(1, 2)); // 3
console.log(add(5, 10)); // 15
지원 타입: 'number', 'string', 'boolean', 'null', 'array'(ptr, size)
포인터 전달 패턴
// C++: 버퍼를 받아 처리
EMSCRIPTEN_KEEPALIVE
void process_buffer(uint8_t* buf, int len) {
for (int i = 0; i < len; ++i) {
buf[i] = buf[i] ^ 0xFF; // XOR
}
}
// JS: malloc → 데이터 복사 → 호출 → free
// 변수 선언 및 초기화
const data = new Uint8Array([1, 2, 3, 4, 5]);
const ptr = Module._malloc(data.length);
Module.HEAP8.set(data, ptr);
Module._process_buffer(ptr, data.length);
const result = Module.HEAP8.subarray(ptr, ptr + data.length);
Module._free(ptr);
undefined 모듈, file:// CORS, memory out of bounds 같은 에러
문제 1: “Cannot read property ‘_add’ of undefined”
원인: Module이 아직 초기화되지 않은 상태에서 Module._add를 호출함.
해결법:
// ❌ 잘못된 예
console.log(Module._add(1, 2)); // Module이 준비 전
// ✅ 올바른 예
Module.onRuntimeInitialized = () => {
console.log(Module._add(1, 2));
};
문제 2: “CORS policy” 에러 (file:// 프로토콜)
원인: 로컬에서 file://로 HTML을 열면 Wasm 로드 시 CORS 에러 발생.
해결법:
# 반드시 웹 서버로 실행
python -m http.server 8000
# 또는
npx serve .
# 브라우저에서 http://localhost:8000/hello.html
문제 3: “Memory out of bounds” / “Invalid memory access”
원인: 할당된 범위를 벗어난 메모리 접근. malloc 크기 부족, 잘못된 오프셋 등.
해결법:
// ❌ 잘못된 예: size보다 많이 쓰기
EMSCRIPTEN_KEEPALIVE
void bad_copy(char* dst, const char* src, int len) {
for (int i = 0; i <= len; ++i) // 오프바운드!
dst[i] = src[i];
}
// ✅ 올바른 예: 범위 검사
EMSCRIPTEN_KEEPALIVE
void safe_copy(char* dst, const char* src, int len) {
for (int i = 0; i < len; ++i)
dst[i] = src[i];
}
문제 4: 메모리 누수 (Memory leak)
원인: C++에서 malloc/new로 할당한 메모리를 JS에 넘기고 free/delete를 호출하지 않음.
해결법:
// ❌ 잘못된 예
const ptr = Module._malloc(1024);
// ... 사용 후 free 안 함 → 누수
// ✅ 올바른 예
const ptr = Module._malloc(1024);
try {
// 사용
} finally {
Module._free(ptr);
}
문제 5: “exported function not found”
원인: EMSCRIPTEN_KEEPALIVE 누락 또는 EXPORTED_FUNCTIONS에 함수명이 없음.
해결법:
// ✅ EMSCRIPTEN_KEEPALIVE 필수
extern "C" {
EMSCRIPTEN_KEEPALIVE
int my_func(int x) { return x * 2; }
}
# 또는 빌드 시 명시
emcc main.cpp -o out.js -s EXPORTED_FUNCTIONS=['_my_func']
문제 6: 동기 함수로 브라우저 멈춤
원인: fetch, XMLHttpRequest 등을 동기적으로 호출하면 메인 스레드가 블로킹됩니다.
해결법: Emscripten에서 네트워크·파일 로드는 비동기로만 사용. ASYNCIFY 옵션으로 동기 코드를 비동기로 변환할 수 있으나, 번들 크기가 커짐.
문제 7: SharedArrayBuffer 관련 에러 (멀티스레딩)
원인: Wasm 스레드 사용 시 SharedArrayBuffer가 필요한데, Cross-Origin Isolation 헤더가 없으면 사용 불가.
해결법:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
서버에서 위 헤더를 응답에 포함해야 합니다.
export 최소화, MODULARIZE, 디버그·릴리스 빌드 분리
export 최소화
필요한 함수만 export해 번들 크기와 보안을 유지합니다.
emcc main.cpp -o out.js -s EXPORTED_FUNCTIONS=['_init','_process','_cleanup']
MODULARIZE + EXPORT_NAME 사용
ES 모듈 형태로 로드하면 await createModule()로 비동기 초기화가 깔끔합니다.
emcc main.cpp -o out.js -s MODULARIZE=1 -s EXPORT_NAME='createModule'
const createModule = await import('./out.js');
const Module = await createModule.default();
메모리 할당 일원화
C++에서 할당한 메모리는 C++에서 해제하는 패턴을 유지합니다. JS에 포인터만 넘길 때는 “호출자가 free” 규칙을 문서화합니다.
에러 처리
C++에서 에러 코드를 반환하며, JS에서 검사합니다.
EMSCRIPTEN_KEEPALIVE
int process(int* out_result) {
if (invalid_input) return -1; // 에러 코드
*out_result = 42;
return 0; // 성공
}
디버그 빌드와 릴리스 빌드 분리
# 디버그: 소스맵, 최적화 없음
emcc main.cpp -o out.js -g -O0 -s ASSERTIONS=2
# 릴리스: 최적화, 크기 최소화
emcc main.cpp -o out.js -O3 -s ASSERTIONS=0 -s ENVIRONMENT='web'
디버깅 팁
- 소스맵:
-g옵션으로 빌드하면 브라우저 개발자 도구에서 C++ 소스에 브레이크포인트를 걸 수 있습니다. - 로그:
emscripten/console.h의emscripten_log()또는printf를 사용하면 브라우저 콘솔에 출력됩니다. - 메모리 덤프:
Module.HEAP8.buffer를 확인해 Wasm 메모리 상태를 점검할 수 있습니다. - 함수 export 확인: 빌드 시
-s EXPORTED_FUNCTIONS='[...]'와--emit-symbol-map를 사용하면 export된 심볼 목록을 확인할 수 있습니다.
#include <emscripten/console.h>
EMSCRIPTEN_KEEPALIVE
void debug_example(int x) {
emscripten_log(EM_LOG_CONSOLE, "C++ received: %d", x);
}
CDN 캐싱, 로딩 UI, Worker 실행, 번들 크기 줄이기
패턴 1: CDN + 캐싱
Wasm 파일은 변경 빈도가 낮으므로 CDN에 배치하며, Cache-Control로 장기 캐싱합니다.
<script src="https://cdn.example.com/wasm/engine.js" crossorigin></script>
패턴 2: 로딩 상태 UI
Wasm 초기화에 1~3초 걸릴 수 있으므로, 로딩 스피너나 프로그레스 바를 표시합니다.
const status = document.getElementById('status');
status.textContent = 'Wasm 로딩 중...';
const Module = await createModule();
status.textContent = '준비 완료';
패턴 3: 폴백 (Wasm 미지원 브라우저)
if (!WebAssembly) {
console.warn('WebAssembly 미지원. JS 폴백 사용.');
// 순수 JS 구현으로 대체
} else {
const Module = await createModule();
// Wasm 사용
}
패턴 4: Worker에서 실행
무거운 연산을 메인 스레드에서 분리해 UI 블로킹을 방지합니다.
// worker.js
import createModule from './engine.js';
const Module = await createModule();
self.onmessage = (e) => {
const result = Module._process(e.data);
self.postMessage(result);
};
패턴 5: 번들 크기 최적화
emcc main.cpp -o out.js \
-O3 -Os \
-s WASM=1 \
-s ENVIRONMENT='web' \
-s FILESYSTEM=0 \
-s NO_EXIT_RUNTIME=1 \
-s STANDALONE_WASM=0 \
-s MALLOC='emmalloc' \
-s EXPORTED_FUNCTIONS=['_main','_process']
| 옵션 | 효과 |
|---|---|
-Os | 크기 우선 최적화 |
FILESYSTEM=0 | 파일 시스템 제거 |
NO_EXIT_RUNTIME=1 | 종료 코드 제거 |
MALLOC=emmalloc | 가벼운 malloc 사용 |
패턴 6: 빌드 옵션 요약표
| 옵션 | 용도 | 예시 |
|---|---|---|
-O3 / -Os | 최적화 수준 | -O3 속도, -Os 크기 |
-s WASM=1 | Wasm 출력 (기본) | asm.js 대신 Wasm |
-s MODULARIZE=1 | ES 모듈 형태 | await createModule() |
-s EXPORTED_FUNCTIONS | export 함수 목록 | ['_main','_process'] |
-s EXPORTED_RUNTIME_METHODS | ccall, cwrap 등 | ['ccall','cwrap'] |
-s FILESYSTEM=0 | FS 제거 (크기 감소) | 파일 미사용 시 |
-lembind | Embind 링크 | C++ 클래스 → JS |
성능 비교: JavaScript vs Wasm
피보나치 재귀 같은 마이크로 벤치마크로 JS와 Wasm을 비교하면 결과가 기대와 다르게 나오는 경우가 많습니다. 최신 브라우저의 JS 엔진은 뜨거운 함수를 JIT으로 기계어까지 컴파일하기 때문에, 정수 연산만 하는 짧은 함수는 JS도 충분히 빠릅니다. 또 “순수 재귀 vs 반복문”처럼 알고리즘이 다르면 언어 차이가 아니라 알고리즘 차이를 재게 됩니다.
| 구분 | Wasm이 유리한 경우 | JS와 차이가 작거나 불리한 경우 |
|---|---|---|
| 연산 성격 | 큰 배열을 반복 처리하는 이미지·오디오 처리, 행렬 연산, 물리 시뮬레이션, 압축·코덱 | 짧은 정수 연산 함수, DOM 조작 위주 로직 |
| 이유 | 타입이 정적으로 정해져 있어 JIT 워밍업·타입 추측·역최적화(deopt)가 없고, 선형 메모리를 직접 다뤄 GC 부담이 없음. SIMD 확장도 활용 가능 | JS 엔진도 JIT으로 충분히 최적화하며, JS↔Wasm 경계를 자주 넘으면 호출·데이터 복사 비용이 이득을 상쇄 |
요약: Wasm의 강점은 “모든 코드가 몇 배 빠르다”가 아니라 예측 가능한 성능과 기존 C++ 코드 재사용입니다. 도입 전에는 실제 처리할 데이터로 JS 구현과 Wasm 구현을 같은 알고리즘으로 비교하고, JS와 Wasm 사이에서 큰 버퍼를 매번 복사하지 않도록 경계를 설계하세요.
어디에 쓰고 어디서 막히나: 활용 사례와 제약
활용
- 게임/엔진: 기존 C++ 게임을 웹으로 포팅할 때 Emscripten이 표준적으로 사용됩니다. (예: Unity, Unreal 등도 Wasm 타겟 지원)
- 코덱·처리: FFmpeg, 이미지 리사이즈 등 C/C++ 라이브러리를 Wasm으로 빌드해 브라우저에서 실행.
- 도구·편집기: C++로 만든 오프라인 도구의 핵심만 Wasm으로 옮겨 웹 앱으로 제공.
실무 활용 사례
온라인 이미지 편집기: Photopea 같은 웹 기반 이미지 편집 툴은 C++로 작성된 이미지 처리 라이브러리를 Wasm으로 컴파일해 브라우저에서 실행합니다. 서버 업로드 없이 클라이언트에서 모든 처리가 가능해 속도와 프라이버시가 개선됩니다. CAD/3D 뷰어: AutoCAD Web, Fusion 360 등은 C++ 기반 렌더링 엔진을 Wasm으로 포팅해 브라우저에서 3D 모델을 로드하고 조작할 수 있게 합니다. 별도 플러그인 설치 없이 웹에서 바로 실행됩니다. 암호화/보안: OpenSSL, libsodium 같은 C 라이브러리를 Wasm으로 빌드해 클라이언트 측 암호화를 구현하면, 민감한 데이터를 서버에 보내기 전에 브라우저에서 암호화할 수 있습니다.
제약
- 파일 시스템: 브라우저에는 실제 파일 시스템이 없으므로, Emscripten이 제공하는 가상 FS(MEMFS, IDBFS 등)를 쓰거나, JS 쪽에서 버퍼를 넘겨 주는 방식이 필요합니다.
- 스레드: Wasm 스레드는 SharedArrayBuffer 등 브라우저 정책과 맞물려 있어, 환경·헤더 설정이 필요합니다.
- 코드 크기: STL·런타임을 포함하면 번들 크기가 커질 수 있어, 필요한 것만 링크하거나 스플리팅하는 전략이 필요합니다.
번들 크기 최적화 팁
최적화 플래그 사용:
emcc hello.cpp -o hello.js -O3 -s WASM=1 -s MODULARIZE=1
- -O3: 최대 최적화 (코드 크기와 속도 균형)
- -Os: 크기 우선 최적화 (번들 크기 최소화)
- -s NO_EXIT_RUNTIME=1: 런타임 종료 코드 제거
- -s FILESYSTEM=0: 파일 시스템 미사용 시 제거
크기가 얼마나 줄어드는지는 코드가 무엇을 끌어오느냐에 따라 크게 다릅니다.
iostream을 쓰면 로캘·스트림 관련 코드가 통째로 링크되어printf만 쓸 때보다 결과물이 훨씬 커지고, 최적화 없는(-O0) 빌드는 디버그 정보와 인라인되지 않은 함수 때문에 가장 큽니다. 플래그를 바꿀 때마다ls -l *.wasm으로 크기를 확인하고, 무엇이 크기를 차지하는지는wasm-objdump -h나twiggy top같은 도구로 보는 것이 추측보다 정확합니다.
단계별 실습: 첫 Wasm 앱 만들기
1단계: Emscripten 설치 (10분)
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh
2단계: C++ 코드 작성 (2분)
// calc.cpp
#include <emscripten.h>
extern "C" {
EMSCRIPTEN_KEEPALIVE
int fibonacci(int n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
3단계: 컴파일 (1분)
emcc calc.cpp -o calc.html -O3 -s WASM=1
4단계: 실행 (1분)
python -m http.server 8000
# 브라우저에서 http://localhost:8000/calc.html 열기
소요 시간: 설치 제외 약 4분이면 브라우저에서 C++ 코드를 실행할 수 있습니다.
첫 Wasm 앱 점검 항목
- Emscripten SDK 설치 및
emsdk_env.sh활성화 -
extern "C"+EMSCRIPTEN_KEEPALIVE로 export 함수 정의 -
Module.onRuntimeInitialized콜백에서 Wasm 함수 호출 -
malloc/free쌍으로 메모리 누수 방지 - 로컬 테스트 시
python -m http.server사용 (CORS 방지) - 프로덕션 빌드 시
-O3또는-Os적용 - Wasm 미지원 브라우저 폴백 구현
다음 단계로 나아가기
이 글을 마스터했다면:
- Embind: C++ 클래스를 JS에 노출하는 고급 바인딩
- 파일 시스템: IDBFS로 브라우저 저장소 활용
- 멀티스레딩: pthread를 Wasm으로 컴파일 관련 글: HTTP 클라이언트(#21-1), 프로토콜 설계(#30-3)
같이 보면 좋은 글
- C++와 Rust: 두 언어의 상호 운용성과 Memory Safety 논쟁의 실체 [#44-2]
- C++ vs Rust: 소유권, 메모리 안전성, 에러 처리, 동시성, 성능 비교
- C++ 컨테이너 기반 개발: Docker로 빌드 환경 표준화 및 배포 이미지 최적화 [#40-3]
- C++ Python과 C++의 만남 | pybind11으로 고성능 엔진 만들기 [#35-1]
- C++ Data Race
- C++ 캐시 히트(Cache Hit)를 높이는 메모리 정렬과 패딩 | False Sharing 해결
- C++ Lock-Free 프로그래밍 실전 | CAS·ABA·메모리 순서·고성능 큐 [#34-3]
- C++ 현대적인 C++ GUI: Dear ImGui로 디버깅 툴·대시보드 만들기 [#36-1]
자주 묻는 질문 (FAQ)
Q. HTML 파일을 더블클릭해 열면 Wasm 로드가 실패하는 이유는 무엇인가요?
A. file:// 프로토콜로 연 페이지에서는 브라우저가 CORS 정책 때문에 .wasm 파일을 fetch로 불러오지 못하게 막습니다. python -m http.server나 Emscripten의 emrun처럼 로컬 HTTP 서버를 띄워 http://로 열어야 합니다. 실제 서버에 배포할 때는 .wasm 파일이 application/wasm MIME 타입으로 제공되는지도 확인합니다.
Q. Wasm과 asm.js의 차이는?
A. asm.js는 JavaScript의 엄격한 서브셋으로, JS 엔진이 최적화해 네이티브에 가까운 속도를 냅니다. Wasm은 바이너리 포맷으로 더 작고 빠르며, 현재 Emscripten 기본 출력은 Wasm입니다. 구형 브라우저 지원이 필요하면 -s WASM=0으로 asm.js를 출력할 수 있습니다.
Q. Node.js에서도 Wasm을 쓸 수 있나요?
A. 네. Node.js는 WebAssembly를 지원하므로, Module.onRuntimeInitialized 콜백에서 Module._add 등을 호출하면 됩니다. 서버 사이드에서 C++ 라이브러리를 활용할 때 유용합니다.
Emscripten으로 C++을 Wasm으로 빌드해 브라우저에서 실행할 수 있습니다. 다음으로 Dear ImGui(#36-1)를 읽어보면 좋습니다.
이전 글: C++ 실무 융합 #35-1: pybind11
다음 글: [C++ GUI #36-1] 현대적인 C++ GUI: Dear ImGui로 디버깅 툴·대시보드 만들기