C++ WebAssembly(Wasm)와 Emscripten | C++을 브라우저에서 돌리기 [#35-2]
이 글의 핵심
C++ WebAssembly(Wasm)와 Emscripten에 대한 실전 가이드입니다. C++을 브라우저에서 돌리기 [#35-2] 등을 예제와 함께 상세히 설명합니다.
들어가며: C++ 코드를 브라우저에서 돌리고 싶어요
프론트엔드까지 닿는 C++
WebAssembly(Wasm)(웹어셈블리—브라우저에서 네이티브에 가까운 속도로 실행되는 저수준 바이너리 포맷. JS보다 빠른 연산이 필요할 때 사용)는 브라우저가 네이티브에 가까운 속도로 실행할 수 있는 바이너리 포맷입니다. C/C++를 Wasm으로 컴파일하면, 서버 없이 브라우저 안에서 기존 C++ 라이브러리나 게임 엔진을 돌릴 수 있습니다.
Emscripten은 LLVM 기반으로 C/C++ 소스를 Wasm(또는 asm.js)과 JavaScript 글루 코드(JS와 Wasm을 이어 주는 코드)로 바꿔 주는 툴체인(컴파일·링크 등 빌드 도구 묶음)입니다. 메인스트림 브라우저가 Wasm을 지원한 이후, “C++로 작성한 로직을 웹에 그대로 가져오기” 수요가 커졌고, 프론트엔드 개발자들도 검색·도입을 많이 합니다.
추가 문제 시나리오
시나리오 1: 이미지 처리 병목
웹 앱에서 4K 이미지 리사이즈·필터 적용 시 JavaScript Canvas API만으로는 수 초가 걸립니다. C++ OpenCV나 libvips를 Wasm으로 컴파일하면 수백 ms 수준으로 단축됩니다.
시나리오 2: 게임 엔진 포팅
기존 C++ 게임을 웹에서 실행해야 할 때, Unity·Unreal처럼 별도 런타임 대신 핵심 로직만 Wasm으로 빌드해 번들 크기를 줄일 수 있습니다.
시나리오 3: 암호화 라이브러리 활용
OpenSSL, libsodium 같은 검증된 C 라이브러리를 브라우저에서 직접 실행해, 민감한 데이터를 서버에 보내기 전에 클라이언트에서 암호화할 수 있습니다.
시나리오 4: 레거시 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
이 글에서 다루는 것:
- WebAssembly와 Emscripten이 무엇인지, 왜 쓰는지
- 최소 빌드: C++ 파일 하나를 Wasm + JS로 컴파일
- 브라우저에서 호출: JS에서 모듈 로드·함수 호출
- 완전한 예제: C++ → Wasm, JS 인터롭, 메모리 관리
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스와 프로덕션 패턴
- 실전 활용: 기존 C++ 프로젝트를 웹에 올리는 흐름, 제약
목차
- WebAssembly와 Emscripten이란
- Emscripten 설치와 최소 빌드
- 브라우저에서 Wasm 호출하기
- 완전한 WebAssembly/Emscripten 예제
- 메모리 관리와 JS 인터롭
- 자주 발생하는 에러와 해결법
- 베스트 프랙티스
- 프로덕션 패턴
- 실전 활용과 제약
- 정리
개념을 잡는 비유
이 글의 주제는 여러 부품이 맞물리는 시스템으로 보시면 이해가 빠릅니다. 한 레이어(저장·네트워크·관측)의 선택이 옆 레이어에도 영향을 주므로, 본문에서는 트레이드오프를 숫자와 패턴으로 정리합니다.
1. 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++ 코드를 웹에 이식할 때 (게임, 시뮬레이션, 코덱, 암호 라이브러리 등).
- 무거운 연산을 브라우저에서 돌리고 싶을 때 (이미지 처리, 물리 연산, 번역 엔진 등).
- 한 코드베이스로 데스크톱·웹을 동시에 타겟할 때.
2. 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 초기화가 포함됩니다. 로컬 웹 서버로 열어서 동작을 확인할 수 있습니다.
3. 브라우저에서 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”를 참고하면 됩니다.
4. 완전한 WebAssembly/Emscripten 예제
예제 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
5. 메모리 관리와 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);
6. 자주 발생하는 에러와 해결법
문제 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
서버에서 위 헤더를 응답에 포함해야 합니다.
7. 베스트 프랙티스
1. export 최소화
필요한 함수만 export해 번들 크기와 보안을 유지합니다.
emcc main.cpp -o out.js -s EXPORTED_FUNCTIONS=['_init','_process','_cleanup']
2. 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();
3. 메모리 할당 일원화
C++에서 할당한 메모리는 C++에서 해제하는 패턴을 유지합니다. JS에 포인터만 넘길 때는 “호출자가 free” 규칙을 문서화합니다.
4. 에러 처리
C++에서 에러 코드를 반환하고, JS에서 검사합니다.
EMSCRIPTEN_KEEPALIVE
int process(int* out_result) {
if (invalid_input) return -1; // 에러 코드
*out_result = 42;
return 0; // 성공
}
5. 디버그 빌드와 릴리스 빌드 분리
# 디버그: 소스맵, 최적화 없음
emcc main.cpp -o out.js -g -O0 -s ASSERTIONS=2
# 릴리스: 최적화, 크기 최소화
emcc main.cpp -o out.js -O3 -s ASSERTIONS=0 -s ENVIRONMENT='web'
6. 디버깅 팁
- 소스맵:
-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);
}
8. 프로덕션 패턴
패턴 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
동일한 피보나치 계산(예: n=35)을 JavaScript와 C++ Wasm으로 비교한 예시입니다.
| 구현 | 실행 시간 (대략) | 비고 |
|---|---|---|
| JavaScript (순수 재귀) | ~500ms | 인터프리터 오버헤드 |
| JavaScript (메모이제이션) | ~5ms | 최적화된 JS |
| C++ Wasm (순수 재귀) | ~50ms | 네이티브에 가까운 속도 |
| C++ Wasm (반복문) | ~1ms | 최적화된 C++ |
요약: 단순 연산에서는 JS 최적화와 Wasm 차이가 크지 않을 수 있으나, 대량 반복·메모리 접근이 많은 연산(이미지 처리, 행렬 연산, 물리 시뮬레이션)에서는 Wasm이 5~50배 빠른 경우가 많습니다.
9. 실전 활용과 제약
활용
- 게임/엔진: 기존 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: 파일 시스템 미사용 시 제거
실측 예시:
- 기본 빌드: 약 500KB
- -O3 최적화: 약 200KB
- -Os + 불필요한 기능 제거: 약 80KB
단계별 실습: 첫 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++ 코드를 실행할 수 있습니다.
구현 체크리스트
- 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]
- Rust vs C++ 메모리 안전성 | 컴파일러 오류 차이 [#47-3]
- C++ 컨테이너 기반 개발: Docker로 빌드 환경 표준화 및 배포 이미지 최적화 [#40-3]
이 글에서 다루는 키워드 (관련 검색어)
WebAssembly 튜토리얼, Emscripten 사용법, C++ 웹 개발, 브라우저 C++, Wasm 빌드, C++ JavaScript 연동, emcc 컴파일, 웹어셈블리 예제, C++ 웹 포팅 등으로 검색하시면 이 글이 도움이 됩니다.
10. 정리
- WebAssembly는 브라우저에서 네이티브에 가까운 속도로 실행되는 바이너리 포맷이고, Emscripten은 C/C++를 Wasm(+ JS 글루)으로 컴파일하는 툴체인입니다.
- emcc로 C++를 빌드하면 .wasm과 .js가 나오고, 브라우저에서는 JS를 로드한 뒤 Module을 통해 C++에서 export한 함수를 호출할 수 있습니다.
- 메모리 관리:
malloc/free쌍을 지키고, JS에 포인터를 넘길 때 해제 책임을 명확히 합니다. - 자주 하는 실수: 동기 네트워크 호출, 메모리 누수, CORS, export 누락 등을 주의합니다.
- 기존 C++ 프로젝트를 웹에 올릴 때 파일 시스템·스레드·번들 크기를 고려해 설계하면, 프론트엔드와 C++를 융합한 형태의 서비스를 만들 수 있습니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. 이미지 처리, 게임 포팅, 암호화, 레거시 C++ 도구 웹화 등에서 Wasm이 유용합니다. 위 본문의 예제와 프로덕션 패턴을 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. Emscripten 공식 문서, WebAssembly MDN을 참고하세요.
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로 디버깅 툴·대시보드 만들기
실전 체크리스트
실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.
코드 작성 전
- 이 기법이 현재 문제를 해결하는 최선의 방법인가?
- 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
- 성능 요구사항을 만족하는가?
코드 작성 중
- 컴파일러 경고를 모두 해결했는가?
- 엣지 케이스를 고려했는가?
- 에러 처리가 적절한가?
코드 리뷰 시
- 코드의 의도가 명확한가?
- 테스트 케이스가 충분한가?
- 문서화가 되어 있는가?
이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.
참고 자료
- Emscripten 공식 문서
- WebAssembly MDN 가이드
- Emscripten Exporting functions
- Embind 바인딩 가이드
- Emscripten 메모리 관리
관련 글
- 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]