WebAssembly가 실제로 어디에 쓰이고 무엇을 가능하게 하며, 성능·보안·이식성 관점에서의 장점과 한계, 그리고 표준화/생태계의 현재를 정리한다.
URL: https://emnudge.dev/blog/what-happened-to-webassembly/
Title: WebAssembly에 무슨 일이 있었나
목차
WebAssembly에 대한 모든 논의에는, 필연적으로 (대개 상단 근처에) “그 뒤로 WebAssembly에 무슨 일이 있었나요?”라고 묻는 댓글이 하나쯤 달립니다.
세상을 바꿀 혁신처럼 광고되었던 것 같은데, 그저 과장 광고였을까요? JVM 애플릿 같은 또 하나의 시나리오였고, 결국 실패할 운명이었을까요?
저는 이 질문을 다소 이상할 만큼 우회적인 방식으로 다뤄 보려 합니다. 이런 질문들이 몇 가지 중요한 오해를 전제로 하고 있다고 생각하기 때문이고, 그걸 먼저 분명히 해 둘 필요가 있습니다.
물론 WebAssembly는 현실 세계에서 실제로 사용되고 있습니다. 몇 가지 예를 들어 보죠!
이들 중 다수에서 WebAssembly는 제품 전체 또는 핵심 기능에 결정적으로 중요합니다.
하지만 이것만으로는 충분히 설득력이 없다고 생각합니다. 아직 WebAssembly 기반 프레임워크로 전부 만들어진 대형 웹사이트를 흔히 보지는 못합니다. 최대한의 이식성을 위해 애플리케이션을 WebAssembly로 직접 빌드하고 있지도 않죠. 그런데 왜 그럴까요?
이를 답하려면 WebAssembly가 무엇인지에 대한 좋은 멘탈 모델이 필요합니다. 그래야 어디에서 가장 큰 영향을 발휘하는지, 그리고 우리가 맞닥뜨린 한계가 무엇인지 가늠할 수 있습니다.
한마디로, WebAssembly는 하나의 언어입니다.
이 때문에 “WebAssembly는 얼마나 빠른가” 같은 질문은 답하기가 좀 어렵습니다. 대수 표기법이 얼마나 빠른지 묻지는 않잖아요. 그건 그다지 말이 되지 않는 질문입니다.
JavaScript 같은 것의 맥락에서 보면, 언어는 그 언어를 실행하는 엔진만큼만 빠릅니다. JavaScript라는 언어 자체에는 속도가 없지만, V8·SpiderMonkey·JavaScriptCore 같은 JS 엔진은 벤치마크할 수 있습니다. Bun·Deno·Node 같은 JS 런타임의 IO 라이브러리도 벤치마크할 수 있고요.
사람들이 실제로 의미하는 것은 “이 언어의 구성 요소가 현대 하드웨어에 효율적으로 매핑되기에 얼마나 유용한가”와 “이 구성 요소를 활용하는 시스템의 현황은 어떤가”입니다.
충분히 영리한 엔지니어링을 하면, 어떤 시스템이든 몇 가지 트레이드오프를 감수하는 대신 충분히 빠르게 만들 수 있습니다. 코드를 C로 직접 컴파일하는 것이 거슬리지 않는다면, JavaScript와 WebAssembly 모두에서 “네이티브에 가까운” 속도를 얻는 것도 가능합니다.
맞습니다. WebAssembly를 컴파일할 수도 있습니다! 반대로 직접 인터프리트하도록 선택할 수도 있는데, 이는 다른 모든 시스템과 마찬가지로 런타임의 몫입니다.
그럼 WebAssembly의 진짜 질문을 던져 봅시다. 이 언어의 구성 요소는 현대 하드웨어에 효율적으로 매핑되기에 얼마나 유용할까요? 결론부터 말하면, 꽤 유용합니다!
WebAssembly는 어셈블리 언어에 꽤 가까운 근사치입니다. 물론 너무 가깝진 않습니다. 어셈블리보다는 더 고수준이죠. 하지만 대부분의 어셈블리 언어로 큰 속도 손해 없이 깔끔하게 컴파일할 수 있을 정도로는 충분히 가깝습니다.
그리고 네, WebAssembly를 손으로 직접 작성할 수도 있습니다! 저는 rustlings 같은 형태의 코스인 watlings를 만들어서, WAT를 손으로 작성하며 간단한 연습문제를 풀 수 있게 했습니다.
WAT는 Wasm에 아주 가까운 근사치입니다. WAT를 Wasm으로 컴파일했다가 다시 WAT로 되돌려도 정보 손실이 거의 없는 거의 1:1 관계입니다(변수 이름과 일부 메타데이터는 잃을 수 있습니다). 생김새는 이렇습니다:
(module ;; import external i32, name it $global_num_import (import "env" "global_num" (global $global_num_import i32)) ;; A function that adds param $a to $global_num_import, returns i32 (func $add_to_global_num (param $a i32) (result i32) ;; The last stack value is the return value (i32.add (local.get $a) (global.get $global_num_import)) ) ;; export local function, name it add_to_global (export "add_to_global" (func $add_to_global_num)))
코드를 읽어 보세요. 익숙하면서도 낯설게 느껴질 겁니다.
함수와 S-식이 있고, import와 export가 있습니다. 하지만 i32.add 같은 명령과 암묵적인 스택 기반 반환 같은 것도 있죠.
Wasm은 바이트코드로, JVMIS(즉 JVM 바이트코드)와 비교하는 것이 아마 가장 적절할 것입니다. 목표와 제약은 비슷하지만, 환경과 보장 사항은 다릅니다.
JVM 바이트코드와 비교하면, Wasm은 API가 훨씬 작고 안전 보장이 더 강합니다. 메모리 관리 전략에 대해 덜 “의견이 많고”, 호스트 환경의 허가 없이 프로그램이 할 수 있는 일에 더 많은 제한이 있습니다.
계산은 할 수 있지만, 메모리와 모든 import는 명시적으로 제공받아야 합니다. 이런 점에서 (혹은 더 널리 쓰이는) 실제 어셈블리 언어와는 꽤 다릅니다.
이 부분은 나중에 다시 돌아오겠습니다.
많은 언어를 Wasm으로 컴파일할 수 있습니다.
그중 대표적인 것들은 Rust, C, Zig, Go, Kotlin, Java, C#입니다. 흔히 인터프리트되는 언어들도 런타임 자체를 WebAssembly로 컴파일한 사례가 있는데, Python, PHP, Ruby 등이 그렇습니다. 또한 AssemblyScript, Grain, MoonBit처럼 오직 WebAssembly로만 컴파일되는 언어들도 많습니다.
이들 중 다수는 가비지 컬렉터를 요구하지 않는 것이 중요합니다. 다른 일부는 GC를 포함하면 도움이 되겠죠. Wasm은 둘 다를 허용합니다(다만 GC 옵션은 훨씬 최근에 들어왔습니다).
브라우저에는 Wasm “엔진”이 내장되어 있어서, 이는 더욱 매력적인 컴파일 타깃이 됩니다. 즉, 별다른 설정 없이도 여러분의 휴대폰과 노트북은 이미 Wasm 프로그램을 실행할 수 있다는 뜻입니다.
JVM이 여러 가지 러너 구현을 가질 수 있는 것처럼, 브라우저와 독립적으로 실행되는 구현도 많습니다. 예를 들면 Wasmtime, WasmEdge, Wasmer 등이 있습니다.
$ Wasmer run cowsay "I am cow" __________< I am cow > ---------- \ ^__^ \ (oo)\_______ (__)\ )\/\ ||----w | || ||
이 언어들은 특정 하드웨어에 과도하게 종속되지 않는 단일 산출물(artifact)을 출력할 수 있습니다. 실행하려면 Wasm 러너만 있으면 되죠(또 JVM 비유입니다).
지금까지는 Wasm이 JVM과 꽤 비슷해 보입니다. 주요 차이는 메모리 관리 전략과 지원 플랫폼 수 정도로 보이죠.
하지만 진짜로 쐐기를 박는 것은 보안 스토리입니다.
WebAssembly는 모든 외부 상호작용을 명시적이고 호스트가 정의한 import로 취급함으로써 공격 표면을 최소화합니다. 앞에서 이야기했죠. “기본 거부(deny-by-default)” 아키텍처, 작은 명령 집합, 숨겨진 제어 흐름 스택(즉 raw 포인터 없음), 선형 메모리가 결합되어 매우 강력한 보안성을 만들어 냅니다.
이 보안성 덕분에 단일 프로세스 안에서 프로세스 수준의 격리(process-like isolation)를 보장할 수 있습니다. Cloudflare는 V8 내부에서 이 특성을 활용해 V8 isolate를 사용하여 신뢰할 수 없는 코드를 매우 효율적으로 실행합니다. 이는 큰 보안 트레이드오프 없이도 상당한 효율 이득을 낸다는 뜻입니다.
별도의 프로세스를 띄우지 않아도 된다면 Wasm 프로그램은 100배 더 빠르게 시작할 수도 있습니다. Wasm 호스팅 영역의 회사인 Fermyon은 서브 밀리초 수준의 스핀업 시간을 내세웁니다.
이 경우 성능은, 보안 보장이 가능하게 해 주는 것의 직접적인 결과입니다.
다른 경우에는, 보안이 기능 지원을 열어 주기도 합니다.
Flash는 멀티미디어 플랫폼으로, 2021년 1월에 모든 주요 브라우저에서 (주로) 보안 문제 때문에 지원이 중단되기 전까지 애니메이션과 게임에 주로 쓰였습니다. Ruffle은 ActionScript를 위한 인터프리터이자 VM으로 동작함으로써 Newgrounds 같은 사이트에서 Flash 경험을 되살렸습니다.
Cloudflare는 CPython을 Wasm으로 빌드한 Pyodide를 사용해, JS 코드와 유사한 보안 보장 하에 Python 코드를 실행할 수 있게 합니다.
Figma는 Wasm으로 컴파일된 QuickJS 엔진에서 플러그인을 실행하여, 브라우저에서 신뢰할 수 없는 사용자 플러그인을 실행합니다.
또 다른 곳에서는, 이 보안성이 극단적인 임베딩 가능성(embeddability)을 허용합니다.
Wasm 프로그램을 실행하는 다양한 방법을 살펴봤습니다. Wasm 러너는 꽤 가벼울 수 있습니다. 라이브러리 작성자에게 특정 언어(보통 Lua나 JavaScript)를 강제하는 대신, Wasm 자체를 지원하면 훨씬 넓은 선택지가 열립니다.
Zellij, Envoy, Lapce 같은 도구들은 플러그인 생태계를 위해 Wasm을 지원합니다.
이미 JavaScript 엔진이 사용되고 있는 환경에서는, 그렇지 않았다면 실행할 수 없었을 프로그램에 접근할 수 있다는 뜻이기도 합니다.
여기에는 이미지 처리, OCR, 물리 엔진, 렌더링 엔진, 미디어 툴킷, 데이터베이스, 파서 등 수많은 것들이 포함됩니다.
이들 대부분의 경우, Wasm 사용은 여러분에게 투명하게 느껴질 겁니다. 설치한 라이브러리가 의존성 트리 어딘가에서 Wasm을 쓰고 있을 뿐이죠.
Godot와 Figma는 C++로 작성된 코드베이스를 갖고 있지만, (WebAssembly로 컴파일하거나 WebAssembly와 조합하여) 종종 브라우저에서 바로 동작할 수 있습니다.
Wasm의 가장 흔한 용도는 언어 간 격차를 메우는 것처럼 보입니다. 어떤 생태계들은 특정 도구 모음이 더 풍부하죠. Squoosh는 NPM에서 고를 수 있는 이미지 압축 라이브러리만 쓸 수 있었다면 훨씬 제한적인 앱이 되었을 겁니다.
브라우저는 JavaScript를 실행하는 것과 거의 같은 파이프라인으로 WebAssembly를 실행합니다. 이는 Wasm 애플리케이션 성능에 하드한 상한을 두는 것처럼 보이지만, 실제 성능은 아키텍처나 도메인에 따라 더 좋거나 더 나쁠 수 있습니다.
더 풍부한 타입 시스템과 더 정교한 최적화 컴파일러를 갖춘 언어를 사용하면 더 효율적인 프로그램을 만들 수 있습니다. V8 같은 엔진의 JIT 모델은, 최적화 비용이 최적화된 코드 실행으로 얻는 이득을 초과하면 최적화를 하지 못하게 만들 수 있습니다. JavaScript를 피함으로써 메가모픽 함수를 더 쉽게 피할 수도 있고요.
하지만 호스트 프로그램 경계를 넘나드는 데에는 비용이 듭니다. 특히 메모리를 복제해야 한다면 더 그렇죠. 이 관점에서 Zaplib의 포스트모템은 흥미로운 글입니다. 코드베이스를 점진적으로 Wasm으로 옮기는 과정은 경계 넘나들기 비용이 크게 발생해, 단기적으로는 어떤 이점도 남기지 못할 수 있습니다.
작은 API 표면은 또한 바이너리 비대화를 의미하기도 합니다. 시스템 API를 import하는 대신 다시 구현하는 경우가 더 많기 때문입니다. 이를 돕기 위한 WASI 같은 표준이 있습니다. 그래도 네이티브 문자열 타입은 아직 없습니다(아직은).
주류 언어 중에서는 Zig가 가장 작은 Wasm 바이너리를 만들어 내는 편으로 보입니다.
네이티브 컨텍스트(즉 JS 엔진 밖)에서 Wasm의 실용적인 성능은 여러 이유로 고전하는 듯합니다. 스레딩과 어떤 형태의 IO든 비용이 들고, 메모리 사용량은 더 크며, 콜드 스타트도 더 느립니다.
그럼에도 이런 성능 트레이드오프가 중요하지 않을 수도 있습니다. 대부분의 용도에서는 “충분히 빠르다”라고 저는 생각합니다. 성능이 매우 민감한 환경이라면, Wasm의 이점은 오히려 덜 관련 있을 가능성이 큽니다.
분명히 많은 일들이 벌어지고 있습니다.
Wasm IO 유튜브 채널에는 볼 만한 발표가 많이 있습니다.
사실 Wasm의 표준과 언어 개발은 내부적으로 상당한 논란도 일으켰습니다. 발전에 대한 욕구는 크지만, 표준화는 결정을 되돌리기 어렵게 만듭니다. 많은 사람들은 변화가 너무 빠르고 방향이 잘못되었다고 느낍니다.
“더 공식적인” W3C 워킹 그룹이 있고, 그다음으로 “덜 공식적인” Bytecode Alliance가 있습니다. Bytecode Alliance는 훨씬 빠르게 움직이며, Wasm 자체가 아니라 Wasm 바깥의 도구와 언어 개발(예: WIT와 WebAssembly 컴포넌트 모델)에 중심을 둡니다.
Wasm 기능 제안은 매우 빠르게 발전하고, 다양한 도구들에 채택되고 있습니다. 표준화로서는 놀라운 진전이지만, 큰 실수가 생길까 두려워하는 사람들에게는 지켜보는 것 자체가 무서울 수도 있습니다.
그렇다면 왜 사람들은 “아무 일도 일어나지 않았다”고 생각할까요?
저는 많은 사람들이 이 기술의 발전이 자신의 업무에 더 눈에 띄는 영향을 줬을 거라고 생각하기 때문이라고 봅니다. 즉, Wasm 도구를 의식적으로 찾아서 사용하게 될 것이라고요.
많은 사람들은 브라우저 안에서 Wasm이 JavaScript를 대체하는 경로가 있다고 생각합니다. 즉 .js 파일을 아예 포함하지 않아도 되는 세상이 올 거라고요. 하지만 이는 가능성이 매우 낮습니다.
다만 Blazor나 Leptos 같은 프레임워크는, 생성되는 JS 산출물을 사용자가 인지하지 못하거나 신경 쓰지 않아도 사용할 수 있습니다.
대체로 Wasm 도구는 애플리케이션 개발자보다 라이브러리 작성자들에게 더 많이 채택되어 왔습니다. 내부는 불투명하죠. 아마도 괜찮습니다.
별개로, Wasm에 관해 일부러 교육 자료를 난해하게 만드는 철학은 커뮤니티에 도움이 되지 않는다고 생각합니다. 이 싸움은 몇 번 지고 말았지만요.
일단은 watlings를 한 번 확인해 보세요. 언젠가 확장하겠습니다. 분명히요.