C++ WebAssembly(Wasm)와 Emscripten | C++을 브라우저에서 돌리기 [#35-2]

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

이 글에서 다루는 것:

  • WebAssemblyEmscripten이 무엇인지, 왜 쓰는지
  • 최소 빌드: C++ 파일 하나를 Wasm + JS로 컴파일
  • 브라우저에서 호출: JS에서 모듈 로드·함수 호출
  • 완전한 예제: C++ → Wasm, JS 인터롭, 메모리 관리
  • 자주 발생하는 에러와 해결법
  • 베스트 프랙티스프로덕션 패턴
  • 실전 활용: 기존 C++ 프로젝트를 웹에 올리는 흐름, 제약

목차

  1. WebAssembly와 Emscripten이란
  2. Emscripten 설치와 최소 빌드
  3. 브라우저에서 Wasm 호출하기
  4. 완전한 WebAssembly/Emscripten 예제
  5. 메모리 관리와 JS 인터롭
  6. 자주 발생하는 에러와 해결법
  7. 베스트 프랙티스
  8. 프로덕션 패턴
  9. 실전 활용과 제약
  10. 정리

개념을 잡는 비유

이 글의 주제는 여러 부품이 맞물리는 시스템으로 보시면 이해가 빠릅니다. 한 레이어(저장·네트워크·관측)의 선택이 옆 레이어에도 영향을 주므로, 본문에서는 트레이드오프를 숫자와 패턴으로 정리합니다.


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 호출하기

로드 순서

  1. Wasm은 비동기로 로드되는 경우가 많으므로, Promise 기반 또는 onRuntimeInitialized 콜백을 사용합니다.
  2. Module 객체가 준비된 뒤, Module._add 같은 export된 C 함수를 호출합니다.
  3. 메모리: 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 사용법

ccallcwrap은 타입 변환을 자동으로 처리해 줍니다.

// 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.hemscripten_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=1Wasm 출력 (기본)asm.js 대신 Wasm
-s MODULARIZE=1ES 모듈 형태await createModule()
-s EXPORTED_FUNCTIONSexport 함수 목록['_main','_process']
-s EXPORTED_RUNTIME_METHODSccall, cwrap 등['ccall','cwrap']
-s FILESYSTEM=0FS 제거 (크기 감소)파일 미사용 시
-lembindEmbind 링크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로 디버깅 툴·대시보드 만들기

실전 체크리스트

실무에서 이 개념을 적용할 때 확인해야 할 사항입니다.

코드 작성 전

  • 이 기법이 현재 문제를 해결하는 최선의 방법인가?
  • 팀원들이 이 코드를 이해하고 유지보수할 수 있는가?
  • 성능 요구사항을 만족하는가?

코드 작성 중

  • 컴파일러 경고를 모두 해결했는가?
  • 엣지 케이스를 고려했는가?
  • 에러 처리가 적절한가?

코드 리뷰 시

  • 코드의 의도가 명확한가?
  • 테스트 케이스가 충분한가?
  • 문서화가 되어 있는가?

이 체크리스트를 활용하여 실수를 줄이고 코드 품질을 높이세요.


참고 자료


관련 글

  • 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]