본문으로 건너뛰기
Previous
Next
WebAssembly 완전 가이드 | 웹에서 네이티브 성능 구현하기

WebAssembly 완전 가이드 | 웹에서 네이티브 성능 구현하기

WebAssembly 완전 가이드 | 웹에서 네이티브 성능 구현하기

이 글의 핵심

JavaScript보다 10배 빠른 WebAssembly. C/C++/Rust 코드를 브라우저에서 실행하며, 게임·영상 처리·암호화 등 고성능이 필요한 작업에 최적입니다. JavaScript와 완벽히 상호 운용되는 차세대 웹 표준입니다.

이 글의 핵심

WebAssembly(WASM)는 웹 브라우저에서 네이티브에 가까운 성능을 제공하는 바이너리 포맷입니다. C/C++/Rust로 작성한 코드를 JavaScript보다 10배 빠르게 실행하며, 게임·영상 처리·암호화 등 고성능 작업에 최적화되어 있습니다.

목차

WebAssembly란?

WebAssembly는 2017년 W3C에서 표준화한 저수준 바이너리 포맷입니다.

🚀 핵심 특징

1. 네이티브급 성능

JavaScript:
- 인터프리터 or JIT 컴파일
- 가비지 컬렉션
- 동적 타입

WebAssembly:
- AOT 컴파일된 바이너리
- 수동 메모리 관리
- 정적 타입
→ 10-100배 빠름

2. 멀티 언어 지원

  • C/C++: Emscripten
  • Rust: wasm-bindgen
  • Go: TinyGo
  • AssemblyScript: TypeScript와 유사

3. 안전한 샌드박스

  • 브라우저 보안 모델 내에서 실행
  • 메모리 격리
  • 명시적 권한 필요

4. JavaScript와 상호 운용

// JavaScript에서 WASM 호출
const result = wasmModule.add(10, 20);

// WASM에서 JavaScript 호출
wasmModule.alertUser("Hello from WASM!");

WebAssembly 시작하기

1️⃣ Rust로 WASM 개발 (권장)

Rust 설치

# Rust 설치
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# wasm32 타겟 추가
rustup target add wasm32-unknown-unknown

# wasm-pack 설치
cargo install wasm-pack

프로젝트 생성

# Rust WASM 프로젝트 생성
cargo new --lib hello-wasm
cd hello-wasm

Cargo.toml 설정

[package]
name = "hello-wasm"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"

Rust 코드 작성

// src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

#[wasm_bindgen]
pub struct Calculator {
    value: f64,
}

#[wasm_bindgen]
impl Calculator {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Calculator {
        Calculator { value: 0.0 }
    }

    pub fn add(&mut self, x: f64) {
        self.value += x;
    }

    pub fn get_value(&self) -> f64 {
        self.value
    }
}

빌드

# WASM으로 빌드
wasm-pack build --target web

# 결과물:
# pkg/
#   ├── hello_wasm.js
#   ├── hello_wasm_bg.wasm
#   └── hello_wasm.d.ts

HTML에서 사용

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Hello WASM</title>
</head>
<body>
  <h1>WebAssembly Example</h1>
  <button id="btn">Run WASM</button>
  <div id="result"></div>

  <script type="module">
    import init, { add, greet, Calculator } from './pkg/hello_wasm.js';

    async function run() {
      // WASM 초기화
      await init();

      document.getElementById('btn').addEventListener('click', () => {
        // 함수 호출
        const sum = add(10, 20);
        const greeting = greet('Alice');
        
        // 클래스 사용
        const calc = new Calculator();
        calc.add(5);
        calc.add(10);
        const value = calc.get_value();

        document.getElementById('result').innerHTML = `
          Sum: ${sum}<br>
          Greeting: ${greeting}<br>
          Calculator: ${value}
        `;
      });
    }

    run();
  </script>
</body>
</html>

C/C++로 WASM 개발

Emscripten 설치

# Emscripten 설치
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk

./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh

C 코드 작성

// hello.c
#include <emscripten.h>
#include <stdio.h>

// JavaScript에서 호출 가능
EMSCRIPTEN_KEEPALIVE
int add(int a, int b) {
    return a + b;
}

EMSCRIPTEN_KEEPALIVE
double fibonacci(int n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

int main() {
    printf("Hello from WebAssembly!\n");
    return 0;
}

컴파일

# WASM으로 컴파일
emcc hello.c -o hello.html \
  -s WASM=1 \
  -s EXPORTED_FUNCTIONS='["_add","_fibonacci"]' \
  -s EXPORTED_RUNTIME_METHODS='["cwrap"]'

# 결과물:
# hello.html
# hello.js
# hello.wasm

JavaScript에서 사용

<script src="hello.js"></script>
<script>
Module.onRuntimeInitialized = () => {
  // C 함수 래핑
  const add = Module.cwrap('add', 'number', ['number', 'number']);
  const fibonacci = Module.cwrap('fibonacci', 'number', ['number']);

  console.log(add(10, 20)); // 30
  console.log(fibonacci(10)); // 55
};
</script>

실전 예제: 이미지 필터

Rust로 이미지 처리

// src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn grayscale(data: &mut [u8], width: u32, height: u32) {
    for i in (0..data.len()).step_by(4) {
        let r = data[i] as f32;
        let g = data[i + 1] as f32;
        let b = data[i + 2] as f32;
        
        // 그레이스케일 변환
        let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
        
        data[i] = gray;
        data[i + 1] = gray;
        data[i + 2] = gray;
    }
}

#[wasm_bindgen]
pub fn blur(data: &mut [u8], width: u32, height: u32) {
    let w = width as usize;
    let h = height as usize;
    let mut output = vec![0u8; data.len()];
    
    // 간단한 박스 블러 (3x3)
    for y in 1..h-1 {
        for x in 1..w-1 {
            let idx = (y * w + x) * 4;
            
            for c in 0..3 {
                let mut sum = 0u32;
                for dy in -1i32..=1 {
                    for dx in -1i32..=1 {
                        let ny = (y as i32 + dy) as usize;
                        let nx = (x as i32 + dx) as usize;
                        let i = (ny * w + nx) * 4 + c;
                        sum += data[i] as u32;
                    }
                }
                output[idx + c] = (sum / 9) as u8;
            }
            output[idx + 3] = data[idx + 3]; // 알파 채널 유지
        }
    }
    
    data.copy_from_slice(&output);
}

JavaScript에서 사용

<!DOCTYPE html>
<html>
<head>
  <title>Image Filter</title>
</head>
<body>
  <input type="file" id="fileInput" accept="image/*">
  <canvas id="canvas"></canvas>
  <br>
  <button id="grayscale">Grayscale</button>
  <button id="blur">Blur</button>

  <script type="module">
    import init, { grayscale, blur } from './pkg/image_filter.js';

    let imageData;
    const canvas = document.getElementById('canvas');
    const ctx = canvas.getContext('2d');

    // WASM 초기화
    await init();

    // 이미지 로드
    document.getElementById('fileInput').addEventListener('change', (e) => {
      const file = e.target.files[0];
      const img = new Image();
      
      img.onload = () => {
        canvas.width = img.width;
        canvas.height = img.height;
        ctx.drawImage(img, 0, 0);
        imageData = ctx.getImageData(0, 0, img.width, img.height);
      };
      
      img.src = URL.createObjectURL(file);
    });

    // 그레이스케일 적용
    document.getElementById('grayscale').addEventListener('click', () => {
      if (!imageData) return;
      
      const data = imageData.data;
      const start = performance.now();
      
      // WASM 호출
      grayscale(data, canvas.width, canvas.height);
      
      const end = performance.now();
      console.log(`Grayscale: ${end - start}ms`);
      
      ctx.putImageData(imageData, 0, 0);
    });

    // 블러 적용
    document.getElementById('blur').addEventListener('click', () => {
      if (!imageData) return;
      
      const data = imageData.data;
      const start = performance.now();
      
      // WASM 호출
      blur(data, canvas.width, canvas.height);
      
      const end = performance.now();
      console.log(`Blur: ${end - start}ms`);
      
      ctx.putImageData(imageData, 0, 0);
    });
  </script>
</body>
</html>

성능 벤치마크

JavaScript vs WASM

// JavaScript 버전
function fibonacciJS(n) {
  if (n <= 1) return n;
  return fibonacciJS(n - 1) + fibonacciJS(n - 2);
}

// 벤치마크
console.time('JS');
console.log(fibonacciJS(40)); // 102334155
console.timeEnd('JS');
// JS: 1200ms

console.time('WASM');
console.log(fibonacciWasm(40)); // 102334155
console.timeEnd('WASM');
// WASM: 120ms (10배 빠름)

WASM과 JavaScript 상호 운용

JavaScript 함수 호출 (Rust)

// src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
    // JavaScript 함수 선언
    fn alert(s: &str);
    
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

#[wasm_bindgen]
pub fn greet_with_alert(name: &str) {
    let message = format!("Hello, {}!", name);
    log(&message);
    alert(&message);
}

JavaScript 객체 전달

use wasm_bindgen::prelude::*;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct User {
    name: String,
    age: u32,
}

#[wasm_bindgen]
pub fn process_user(val: JsValue) -> JsValue {
    // JavaScript 객체 → Rust 구조체
    let user: User = serde_wasm_bindgen::from_value(val).unwrap();
    
    // 처리
    let processed = User {
        name: user.name.to_uppercase(),
        age: user.age + 1,
    };
    
    // Rust 구조체 → JavaScript 객체
    serde_wasm_bindgen::to_value(&processed).unwrap()
}
// JavaScript
import { process_user } from './pkg/myapp.js';

const user = { name: 'alice', age: 25 };
const result = process_user(user);
console.log(result); // { name: 'ALICE', age: 26 }

WASM의 실제 사용 사례

1. Figma (디자인 도구)

  • C++로 작성된 렌더링 엔진을 WASM으로 포팅
  • 브라우저에서 네이티브급 성능

2. Google Earth

  • C++로 작성된 3D 엔진을 WASM으로 이식
  • 플러그인 없이 브라우저에서 실행

3. Photoshop Web

  • 이미지 처리 엔진을 WASM으로 최적화
  • 복잡한 필터를 실시간 처리

4. Unity 게임

  • Unity 게임을 WASM으로 내보내기
  • 웹 브라우저에서 3D 게임 실행

5. FFmpeg.wasm

  • 비디오 인코딩/디코딩을 브라우저에서
  • 서버 없이 클라이언트에서 처리

WASM의 한계

WASM이 적합하지 않은 경우

  1. DOM 조작: JavaScript가 훨씬 빠름
  2. 간단한 UI 로직: 오버헤드가 더 큼
  3. 네트워크 I/O: 병목이 네트워크이므로 이점 없음
  4. 작은 계산: WASM 로딩 시간이 더 오래 걸림

WASM이 유용한 경우

  1. 계산 집약적 작업: 암호화, 압축, 수학 연산
  2. 이미지/영상 처리: 필터, 변환, 렌더링
  3. 게임 엔진: 물리 엔진, 충돌 감지
  4. 레거시 코드 포팅: C/C++ 라이브러리를 웹에서 사용

핵심 정리

WebAssembly의 장점

  1. 네이티브급 성능: JavaScript보다 10-100배 빠름
  2. 멀티 언어 지원: C/C++/Rust/Go 등
  3. 안전한 실행: 브라우저 샌드박스 내에서
  4. JavaScript 상호 운용: 기존 코드와 통합 가능
  5. 표준화: 모든 주요 브라우저 지원

🚀 다음 단계


시작하기: cargo new --lib my-wasm 명령으로 5분 만에 첫 WebAssembly 프로젝트를 시작하고, 네이티브급 성능을 경험하세요! 🚀