Claude Code로 WebAssembly 통합하기: Rust, wasm-pack, Vite 실전
Claude Code로 Rust WebAssembly를 Vite에 연결하고 이미지 처리, CSV 계산, 타입 래퍼, 리뷰 프롬프트까지 정리합니다.
Claude Code와 WebAssembly의 역할을 먼저 나누기
WebAssembly, 줄여서 Wasm은 Rust, C, C++ 같은 언어로 작성한 코드를 브라우저나 Node.js에서 실행할 수 있게 해 주는 이진 실행 형식입니다. 초보자가 자주 오해하는 지점은 Wasm을 JavaScript 대체재로 보는 것입니다. 실제 프로젝트에서는 UI, DOM 이벤트, 접근성, 상태 관리는 TypeScript에 남기고, 이미지 처리, 압축, 바이너리 변환, CSV 집계, 수치 계산처럼 CPU를 많이 쓰는 작은 부분만 Wasm으로 옮기는 편이 안전합니다.
Claude Code가 유용한 이유는 Rust 함수 하나를 만들어 주기 때문만이 아닙니다. wasm-pack 빌드, Vite 로딩, 비동기 초기화, TypeScript 래퍼, 벤치마크, 리뷰 기준을 한 번에 맞출 수 있기 때문입니다. “Wasm으로 빠르게 해줘”라고만 요청하면, 작동은 하지만 매 클릭마다 초기화하거나 JavaScript와 Wasm 사이를 너무 자주 오가서 실제로는 느린 코드가 나올 수 있습니다.
이 글에서는 RGBA 이미지 반전, CSV 숫자 열 합계, 바이트 배열 체크섬이라는 세 가지 예제로 최소 구성을 만듭니다. 각각 이미지 처리, 텍스트 파싱, 바이너리 처리의 대표 형태입니다. 이후에는 브라우저 내부 고속 처리, 기존 Rust/C++ 자산 이식, 압축 또는 커스텀 코덱, 서버에 올리기 어려운 민감 데이터의 로컬 계산으로 확장할 수 있습니다. 전체 성능 관점은 Claude Code 성능 최적화와 함께 보면 흐름이 잡힙니다.
공식 자료도 함께 확인하세요. MDN WebAssembly는 플랫폼 기본 개념을, wasm-bindgen Guide는 Rust와 JavaScript의 연결 방식을, wasm-pack repository는 Rust에서 Wasm으로 빌드하는 도구 흐름을 설명합니다.
유스케이스를 정하고 시작하기
Wasm은 넣기만 하면 빨라지는 기술이 아닙니다. JavaScript와 Wasm 사이에는 경계 비용이 있습니다. 경계 비용은 인자와 반환값이 두 실행 환경 사이를 건널 때 생기는 비용입니다. 한 픽셀마다 Wasm 함수를 호출하면 대개 손해입니다. 반대로 큰 배열을 한 번 넘기고 내부에서 한 번에 처리하면 이득을 볼 가능성이 높습니다.
| 유스케이스 | Wasm에 맞는 이유 | Claude Code에 확인시킬 것 |
|---|---|---|
| 이미지 처리 | RGBA 버퍼를 긴 루프로 처리하기 좋음 | 복사 횟수, Canvas 읽기와 쓰기, 동일 입력 벤치 |
| 암호, 압축, 코덱 | 바이트 배열 중심이고 Rust 라이브러리 활용 가능 | 검증된 라이브러리 필요 여부, 직접 구현 금지 범위 |
| CSV와 수치 계산 | 파싱, 집계, 특징량 계산에 반복 루프가 많음 | 빈 행, NaN, 큰 파일, 오류 처리 방식 |
| Rust/C++ 자산 이식 | 기존 비즈니스 로직을 브라우저에서 재사용 가능 | OS API, 파일 I/O, 스레드, 시스템 라이브러리 의존성 |
| 브라우저 내 고속 처리 | 데이터를 서버로 보내지 않고 로컬에서 계산 가능 | 초기 로드 크기, 폴백, 대상 브라우저 |
Masa가 작은 콘텐츠 도구에서 시험했을 때, 기능 전체를 한 번에 Wasm으로 바꾸기보다 한 함수만 옮기고 벤치마크를 먼저 보는 방식이 더 안정적이었습니다. 이미지 처리에서는 Rust 함수가 빨라도 Canvas에서 데이터를 꺼내고 다시 쓰는 시간이 결과를 좌우했습니다. CSV는 줄마다 호출하지 않고 파일 내용을 한 번에 넘기는 쪽이 훨씬 단순했습니다.
Rust와 wasm-pack 최소 구현
wasm-pack은 Rust crate를 빌드하고 wasm-bindgen을 실행해 pkg 디렉터리를 만듭니다. 그 안에는 Wasm 바이너리, JavaScript 로더, package 정보, TypeScript 선언 파일이 들어갑니다. wasm-bindgen은 Rust 함수와 JavaScript 사이를 연결하는 라이브러리입니다. 먼저 의존성을 작게 유지한 예제로 시작합니다.
# Cargo.toml
[package]
name = "wasm-lab"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
// src/lib.rs
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn invert_rgba(pixels: &mut [u8]) {
for chunk in pixels.chunks_exact_mut(4) {
chunk[0] = 255 - chunk[0];
chunk[1] = 255 - chunk[1];
chunk[2] = 255 - chunk[2];
}
}
#[wasm_bindgen]
pub fn sum_csv_column(csv: &str, column: usize) -> f64 {
csv.lines()
.filter(|line| !line.trim().is_empty())
.filter_map(|line| line.split(',').nth(column))
.filter_map(|cell| cell.trim().parse::<f64>().ok())
.sum()
}
#[wasm_bindgen]
pub fn fnv1a32(bytes: &[u8]) -> u32 {
let mut hash = 0x811c9dc5u32;
for byte in bytes {
hash ^= u32::from(*byte);
hash = hash.wrapping_mul(0x01000193);
}
hash
}
rustup target add wasm32-unknown-unknown
cargo install wasm-pack
wasm-pack build --target web --out-dir pkg
여기서 fnv1a32는 학습용 체크섬일 뿐 보안용 해시가 아닙니다. 로그인, 서명, 비밀번호, 결제, 토큰 검증에는 Web Crypto API나 검증된 암호 라이브러리를 사용해야 합니다. 이 예제에서는 바이트 배열 전달 구조를 보기 위해 작은 함수로 둡니다.
Vite에서 로딩하고 타입 래퍼 만들기
빌드 후에는 pkg/wasm_lab.js와 pkg/wasm_lab.d.ts가 생성됩니다. Vite에서는 생성된 모듈을 import하고 init()이 끝난 뒤 공개 함수를 호출합니다. 초기화를 래퍼 안에 숨기면 UI 코드가 Wasm 로딩 방식을 몰라도 되고, 매번 중복 초기화하는 실수를 줄일 수 있습니다.
// src/wasm-client.ts
import init, {
fnv1a32,
invert_rgba,
sum_csv_column,
} from "../pkg/wasm_lab";
export type WasmClient = {
invertImage(imageData: ImageData): Promise<ImageData>;
sumCsvColumn(csv: string, columnIndex: number): Promise<number>;
checksum(bytes: Uint8Array): Promise<number>;
};
let initPromise: Promise<void> | undefined;
async function ensureWasm(): Promise<void> {
initPromise ??= init().then(() => undefined);
return initPromise;
}
export const wasmClient: WasmClient = {
async invertImage(imageData) {
await ensureWasm();
const pixels = new Uint8Array(
imageData.data.buffer,
imageData.data.byteOffset,
imageData.data.byteLength,
);
invert_rgba(pixels);
return imageData;
},
async sumCsvColumn(csv, columnIndex) {
await ensureWasm();
return sum_csv_column(csv, columnIndex);
},
async checksum(bytes) {
await ensureWasm();
return fnv1a32(bytes);
},
};
// src/main.ts
import { wasmClient } from "./wasm-client";
const fileInput = document.querySelector<HTMLInputElement>("#csv-file");
const output = document.querySelector<HTMLPreElement>("#output");
fileInput?.addEventListener("change", async () => {
const file = fileInput.files?.[0];
if (!file || !output) return;
const csv = await file.text();
const total = await wasmClient.sumCsvColumn(csv, 2);
output.textContent = `column 2 total: ${total.toFixed(2)}`;
});
wasm-pack --target web 흐름에서는 우선 표준 Vite 설정으로 시작해도 됩니다. 원시 .wasm 파일을 직접 import하거나 다른 번들 구조를 쓰는 경우에만 플러그인과 top-level await 처리를 따로 검토하세요. 초보 단계의 문제는 대부분 플러그인 부족이 아니라 경로와 초기화 위치입니다.
Claude Code 리뷰 프롬프트
Claude Code에는 구현뿐 아니라 리뷰도 맡겨야 합니다. 단, 리뷰 프롬프트는 범위를 좁혀야 합니다. 비동기 초기화, 메모리 복사, 타입 래퍼, 번들 크기, 벤치마크의 동일 입력 여부를 명시합니다.
Review only these files:
- src/lib.rs
- pkg/wasm_lab.d.ts
- src/wasm-client.ts
- src/main.ts
- src/bench.ts
Goal:
Integrate the Rust WebAssembly module into the Vite app without changing UI behavior.
Check:
1. init() is awaited before any exported Wasm function is called.
2. init() is cached and not repeated for every click or file upload.
3. Large arrays cross the JS-Wasm boundary at most once per user action.
4. DOM updates stay in TypeScript, not inside Rust.
5. The wrapper exposes typed methods and keeps generated pkg files out of hand edits.
6. Benchmarks compare the same input data for JavaScript and Wasm.
Run:
wasm-pack build --target web --out-dir pkg
npm run typecheck
npm run build
팀에서 반복적으로 Wasm을 다룬다면 이 규칙을 CLAUDE.md에 넣어 두는 것이 좋습니다. 매번 누군가가 기억해서 말하는 방식보다, 코드베이스 규칙으로 고정하는 편이 안정적입니다.
간단한 벤치와 테스트 절차
Wasm 전환은 느낌이 아니라 같은 입력으로 측정해야 합니다. 아래 코드는 RGBA 반전을 JavaScript와 Wasm으로 비교합니다. 작지만, 서로 다른 데이터를 측정하는 실수를 잡아낼 수 있습니다.
// src/bench.ts
import { wasmClient } from "./wasm-client";
function invertJs(pixels: Uint8Array): void {
for (let index = 0; index < pixels.length; index += 4) {
pixels[index] = 255 - pixels[index];
pixels[index + 1] = 255 - pixels[index + 1];
pixels[index + 2] = 255 - pixels[index + 2];
}
}
function cloneImageData(source: Uint8Array, width: number, height: number): ImageData {
return new ImageData(new Uint8ClampedArray(source), width, height);
}
export async function runBench(): Promise<void> {
const width = 1920;
const height = 1080;
const source = new Uint8Array(width * height * 4);
crypto.getRandomValues(source);
const jsPixels = new Uint8Array(source);
const wasmImage = cloneImageData(source, width, height);
const jsStart = performance.now();
invertJs(jsPixels);
const jsMs = performance.now() - jsStart;
const wasmStart = performance.now();
await wasmClient.invertImage(wasmImage);
const wasmMs = performance.now() - wasmStart;
console.table({
javascriptMs: Number(jsMs.toFixed(2)),
wasmMs: Number(wasmMs.toFixed(2)),
ratio: Number((jsMs / wasmMs).toFixed(2)),
});
}
wasm-pack build --target web --out-dir pkg
npm run typecheck
npm run build
npm run dev
결과가 기대보다 좋지 않다면 데이터 변환을 먼저 봅니다. Canvas 읽기와 쓰기, ImageData 생성, 문자열 복사, 개발 빌드가 실제 계산 시간을 가릴 수 있습니다. 결과를 Claude Code에 붙여 넣고 Wasm을 유지할지, Web Worker로 옮길지, JavaScript 최적화로 충분한지 물어보면 판단이 쉬워집니다.
자주 만나는 함정
첫째, 초기화는 비동기입니다. init()이 끝나기 전에 내보낸 함수를 호출하면 환경에 따라 간헐적으로 실패합니다. 래퍼에서 promise를 캐시하고 모든 메서드가 먼저 기다리게 만드세요.
둘째, 번들 크기가 커질 수 있습니다. Rust crate를 많이 넣으면 .wasm 크기가 빠르게 늘어납니다. 작은 함수부터 시작하고 프로덕션 빌드 산출물을 확인한 뒤 의존성을 추가합니다.
셋째, JS-Wasm 경계 비용입니다. JavaScript 루프 안에서 Wasm 함수를 반복 호출하면 이득이 사라질 수 있습니다. 배열, 문자열, buffer를 크게 묶어 한 번에 전달합니다.
넷째, Wasm에서 DOM을 직접 다루려는 시도입니다. 이벤트, 렌더링, 접근성 속성, 오류 문구는 TypeScript에 두고 Rust는 순수 계산에 집중하는 편이 테스트하기 쉽습니다.
다섯째, 숨은 메모리 복사입니다. TypedArray, 문자열, ImageData는 바인딩 과정에서 복사될 수 있습니다. 벤치마크에는 변환 비용까지 포함해야 합니다.
여섯째, 브라우저 호환성과 보안 헤더입니다. 일반 Wasm은 널리 지원되지만, Wasm threads와 SharedArrayBuffer는 COOP와 COEP가 필요합니다. 광고, 외부 iframe, CDN 설정이 있는 사이트에서는 특히 일찍 확인해야 합니다.
팀 도입과 상담
개인 실험은 이 코드로 충분합니다. 팀에서 쓰려면 어떤 로직을 Rust로 옮길지, 어떤 코드는 TypeScript에 남길지, 생성된 pkg 파일을 어떻게 관리할지, 어떤 브라우저를 지원할지, 어떤 벤치마크를 통과해야 머지할지 정해야 합니다. 이 내용을 CLAUDE.md에 넣고 Claude Code가 구현과 리뷰를 모두 수행하게 하면 품질이 안정됩니다.
ClaudeCodeLab에서는 실제 저장소를 기준으로 Wasm 적용 대상 선정, Rust/C++ 자산 검토, 벤치 설계, 팀용 프롬프트와 리뷰 규칙 정리를 도와드릴 수 있습니다. WebAssembly가 제품 성능, 개인정보의 브라우저 내 처리, 프런트엔드 공통 구조와 연결된다면 Claude Code 교육 및 상담에서 먼저 범위를 정리하는 것이 좋습니다.
검증 메모
이 흐름을 실제로 따라 해 보니 어려운 부분은 Rust 함수보다 Vite 쪽의 호출 시점이었습니다. init()을 wasm-client.ts에 캐시하자 이미지 처리, CSV 집계, 체크섬이 같은 경로로 동작했습니다. 작은 입력은 JavaScript만으로도 충분히 빨랐고, Full HD 이미지 배열이나 큰 CSV에서야 차이가 보였습니다. 결론은 단순합니다. 함수 본문만 보지 말고 경계 비용과 복사 비용까지 함께 측정해야 합니다.
무료 PDF: Claude Code 치트시트
이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.
개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.
작성자 소개
Masa
Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.
관련 글
Claude Code Permission Receipt Pattern: 권한, 증거, 롤백을 남기는 운영
Claude Code 작업마다 허용 범위, 승인 경계, 검증 명령, 롤백 메모, Gumroad와 상담 CTA 확인을 남기는 permission receipt 패턴입니다.
Claude Code/Codex 안전 Agent Harness 설계: 권한, 검증, 롤백
Claude Code와 Codex를 안전하게 운영하기 위한 Agent Harness를 권한 정책, 실행 계획, 검증, 복구 계층으로 설계합니다.
Claude Code 서브에이전트 실전 가이드: 기사와 코드 작업을 안전하게 위임하기
Claude Code 서브에이전트로 기사와 코드 작업을 안전하게 나누는 방법. 위임 규칙, 프롬프트, 실패 사례를 정리합니다.