Wasm이 웹 기술도 어셈블리 언어도 아니라는 오해를 풀고, 샌드박스 보안과 포터블 바이트코드로서의 특성, 컴파일 타깃으로서의 장점을 사례와 함께 설명합니다.
“Wasm은 WebAssembly의 약자가 아니다”라는 말은 터무니없게 들릴 수 있다. 하지만 webassembly.org만 봐도 그렇지 않다는 걸 알 수 있다:

다행히도 나는 좋은 주장 앞에서 이성에 발목 잡히는 타입이 아니다. Wasm과 ‘WebAssembly’라는 이름의 관계를 이야기해 보자.
Wasm(풀어 쓰면 WebAssembly)의 지지자로서, 나는 사람들에게 그 장점을 설파하는 데 많은 시간을 보낸다. 비누상자 위에 올라 떠들다 보면 Wasm에 대해 흔히 두 가지 오해를 마주친다.
이름만 보면 그럴 법한 오해지만, 이 때문에 Wasm에 익숙하지 않은 사람들이 아예 배제하고 더 깊이 파고들지 않곤 한다. 안타까운 일이다. Wasm은 줄 것이 아주 많다. 하지만 그 어느 것도 웹 기술도, 어셈블리 언어도 아니다. 이 오해를 풀고, 컴파일 타깃으로서 Wasm의 힘을 납득시키고 싶다.
Wasm의 얼굴에서 WebAssembly라는 가면을 벗겨 보면, 사실 바이트코드라는 걸 알게 된다. x86-64나 Arm 같은 어셈블리 언어라기보다, Wasm은 JVM이나 .NET의 바이트코드에 더 가깝다. 바이트코드인 Wasm은 진짜 CPU가 아니라 가상 머신(VM)에서 실행된다. 이 가상 머신에는 브라우저에 특화된 기능이 없다. 이 글을 쓰는 시점에도 Wasm은 DOM과 상호작용하는 방법이 전혀 없다. 서버에서, 아니면 임베디드 기기에서 돌아가는 Wasm VM을 구현할 수도 있다. 유명한 폰트 셰이핑 엔진인 Harfbuzz는 폰트 안에 Wasm을 포함해 커스텀 셰이핑을 수행할 수 있게 한다. 매혹적이면서도 경계되는 사례다.
그렇다면 교묘한 폰트가 악성 코드를 실행해 내 데이터를 훔치거나, 더 나쁘게는 내 동의 없이 뉴스레터에 가입시켜 버리지 않는다는 걸 어떻게 확신할 수 있을까? 비밀은 Wasm의 실행 모델에 있다. 자신을 Wasm 런타임이라 부르고 싶은 VM은 Wasm 사양을 준수해야 한다. 이 사양의 상당 부분은 Wasm 실행이 호스트로부터 샌드박스된다는 사실을 보장하는 데 할애되어 있다. 호스트가 파일시스템 접근, 네트워크 통신 같은 능력을 명시적으로 노출하지 않으면, Wasm은 기본적으로 아무것도 할 수 없다. Harfbuzz는 폰트에 포함된 Wasm에 아무 능력도 노출하지 않기 때문에, 그 Wasm이 악의적인 일을 하지 않으리라 신뢰할 수 있다.
이런 샌드박싱이 제공하는 보안은 Wasm의 주요 강점 가운데 하나다. 임의 코드를 실행하되 해킹당하고 싶지는 않은 곳이라면 어디든 가치를 제공할 수 있다. 그곳은 웹 안팎을 막론하고 생각보다 많다.
그렇다면 대체 무슨 일일까? Wasm이 어셈블리도 아니고, 특별히 웹도 아니라면 왜 그런 이름이 붙었을까? 답은 Wasm의 기원에서 찾을 수 있다. 2015년 무렵, 개발자들은 필사적으로 자바스크립트를 쓰고 싶지 않아 하면서도, 동시에 브라우저에서 앱을 실행하길 간절히 원했다.
브라우저에 새롭고 “더 나은” 언어를 넣으려는 시도가 있었지만 실패했다. 표면 언어를 하나 더 얹는 건 문제를 잠시 미루는 일일 뿐이다. 결국 사람들은 자바스크립트와 더불어 새 언어 역시 피하고 싶어질 것이다. 새 언어는 기존 코드베이스를 웹으로 가져오고 싶은 사람들에게도 아무런 도움이 되지 않는다. 여전히 자바스크립트 대신 새 언어로 앱을 갈아엎어야 하는 껄끄러운 과제가 남기 때문이다.
다른 해법이 필요했다. 브라우저가 자바스크립트만 알아듣는다면, 우리의 컴파일러가 자바스크립트를 말하도록 가르치면 된다. 이 필요에 부응해 asm.js가 등장했다. 컴파일러가 타깃으로 삼도록 고안된 자바스크립트의 부분집합이다. asm.js는 “일반” 자바스크립트보다 네이티브에 훨씬 가까운 성능을 제공하는 기술적 기념비다.
자바스크립트에서 값에 0을 비트 OR, 즉 x|0을 하면 결과가 반드시 정수가 된다는 걸 알았는가? 나도 몰랐다. 하지만 자바스크립트 VM은 알고, 기본의 더 느린 Number 클래스 대신 x를 정수로 최적화한다. asm.js는 이런 온갖 트릭으로 VM이 우리의 컴파일된 언어를 효율적으로 실행하도록 유도한다. 이런 게 가능하다는 건 멋지다. 다만 그것은 아르키메데스의 지레 법칙이 주는 멋짐이 아니라, 루브 골드버그 장치 같은 멋짐이다.
우리의 컴파일된 언어는 이미 그 값이 정수임을 알고 있고, VM도 정수를 어떻게 다뤄야 하는지 안다. 이런 전술을 써야만 하는 건 자바스크립트가 실행을 중재하기 때문이다. 자바스크립트 대신, 모든 브라우저에 들어 있는 VM과 직접 대화할 수 있다면 얼마나 좋을까? 하지만 브라우저는 단일한 VM을 제공하지 않는다. 각 브라우저는 자신만의 VM을 제공한다(V8이 90%의 브라우저에 들어 있다고 해도, 기술적으로 대안이 존재한다). 그리고 거기에 딸린 API 차이 때문에 타깃팅 방식이 달라진다. 자바스크립트는 통합된 인터페이스를 제공하지만, 이미 말했듯 우리의 목적에는 최적이 아니다.
Wasm은 자바스크립트를 타깃팅해야 하는 고통에서 우리를 구해 주었다. 컴파일된 언어에 훨씬 우호적인, 브라우저 실행을 위한 새로운 통합 인터페이스를 제공한 것이다. 이렇게 보면 ‘WebAssembly’라는 이름이 어떻게 탄생했는지 알 수 있다. asm.js의 성과 위에, Wasm은 브라우저를 위한 새로운 저수준 인터페이스를 제공한다.
이런 기원은 이름을 설명해 주지만, 구현 전반을 포괄하지는 못한다. Wasm의 창시자들은 새로운 웹 인터페이스뿐 아니라 범용 계산을 위한 새로운 규격을 만들고자 했다. Wasm의 창시자 중 한 명인 Andreas Rossberg는 ‘WebAssembly’라는 이름이 경영진을 설득해 프로젝트 자금을 따내기 위한 것이었다고 말한다(한 번도 아니고 두 번이나!). Wasm 팀은 처음부터 단지 웹 기술만을 만들 생각이 아니었다.
그들이 Wasm을 위해 설계한 포터블 바이트코드는 적용 범위가 넓고, 자바스크립트가 가진 웹 특화 기술을 전부 배제한다. 또한 꽤 고수준이라 어셈블리보다 훨씬 쉬운 컴파일 타깃이 된다. 예외, 구조체, 고차 함수 같은 기능이 기본 제공되며, 이를 더 저수준의 구성요소로 억지로 매핑할 필요가 없다. 심지어 비동기와 기타 한정된 제어 흐름 패턴을 지원하기 위해 타입드 컨티뉴에이션을 언어에 추가하자는 논의도 있다.
LLVM에서 Wasm을 만들어 낼 수 있어 C, Swift, Rust 등의 언어가 Wasm을 타깃팅할 수 있다. 하지만 이건 잠재력을 다 활용하지 못한다. 오해하지 말자. 이것이 Wasm의 킬러 앱인 건 분명하다. 기존의 Rust 앱을 Wasm으로 컴파일하면 곧장 웹에서 실행된다. LLVM에 의존하는 방대한 코드 자산을 Wasm으로 옮길 수 있는 능력은 추가 투자의 길을 연다.
하지만 LLVM을 Wasm으로 변환하는 일은 다시 자바스크립트를 사이에 두고 말하는 느낌이다. LLVM의 IR은 비구조적 제어 흐름(예: 기본 블록)을 사용하는 반면, Wasm은 전적으로 구조적 제어 흐름(예: while, if, 함수 등)만을 사용한다. 비구조적 제어 흐름을 구조적으로 바꾸려면 Relooper 같은 알고리즘이 필요하다. C, Swift, Rust는 모두 구조적 제어 흐름에서 시작했고, LLVM이 그것을 기본 블록으로 바꿨으며, 우리는 다시 Wasm을 위해 구조적 제어 흐름으로 되돌려야 한다. 기존 언어들에게는 불가피한 필요악이다.
그러나 새로운 언어를 구상하는 컴파일러 저자라면, 이런 구불구불한 우회로 없이 곧장 Wasm을 방출할 수 있다. 이것은 특히 가비지 컬렉션 제안이 표준화되는 지금, 널리 알려지지 않은 Wasm의 이점이다. Wasm은 백엔드를 건너뛰어 매우 고수준 IR을 즉시 실행 가능한 Wasm으로 곧장 바꿀 수 있게 해 준다. 순수한 사랑으로 컴파일러 백엔드를 쓰고 싶다면 좋다. 마음을 따르라. 어차피 이제 컴파일러를 돈 벌려고 쓰는 사람은 없다.
백엔드에는 관심이 없고 빨리 끝내서 타입체커를 다시 만지작거리고 싶은 마음에 LLVM IR을 타깃으로 삼으려 한다면, 더 높은 수준의 타깃인 Wasm을 고려해 보라. 더 빨리 만지작거리러 돌아갈 수 있다. 지금까지 Wasm이 아닌 것들에 대해 많이 이야기했다. 이제 Wasm이 무엇인지도 잠깐 짚고 넘어가자. 자세한 내용은 MDN의 훌륭한 Wasm 소개를 참고하라.
Wasm은 완전히 형식적으로 명세되어 있다. 왜 내 코드가 이상하게 구는지 퍼즐을 맞추듯 고민할 때, 이 명세는 수없이 귀중한 도움이 되었다. 모든 동작이 명세되어 있기 때문에, 비결정성이 발생하는 정확한 지점들을 나열할 수 있다. 그 목록의 기능을 하나도 쓰지 않는다면, 축하한다. 당신의 Wasm은 결정적이다.
겉보기에는 사소해 보일 수 있지만, 컴파일러 작성자에게는 엄청난 이점이다. 표면 문법에서 기계어로 번역하는 과정에서 변동성이 슬그머니 끼어들기 쉽다. 정렬 맵 대신 해시 맵을 사용해 해시 시드에 따라 컴파일러의 출력이 바뀐다든가, 변수에 ID를 할당하는 방식을 안정화하지 않아서 두 변수가 ID 2와 3을 번갈아 갖는다든가 하는 식이다. 충분히 흔한 일이다. Wasm의 결정성 덕분에 버그의 근원이 거기가 아님을 자신할 수 있어, 컴파일러가 왜 깨졌는지 추적할 때 확인해야 할 버그의 거대한 영역을 통째로 걷어낼 수 있다.
버그의 거대한 영역을 걷어낸다는 이야기가 나온 김에, Wasm의 형식적 명세가 낳은 또 다른 결과는 정적 타입이다. Wasm 명령어, Wasm의 함수, 모듈, 내보내기 등은 모두 타입을 갖는다. 이 타입들은 실행 전에 검증되어, 당신의 백엔드가 공들여 토해낸 Wasm이 말이 되는지를 보장한다. 타입이라고 하면 타입 추론이나 오버로드 검사에 시간을 잔뜩 낭비하게 될까 걱정할 수도 있다. 염려 마시라. Wasm 타입은 모두 미리 정의해야 하고, Wasm이 하는 일은 당신의 작업을 확인하는 것뿐이다. 최악의 날에도 빠르게 끝난다.
Wasm은 이륙을 위해 ‘WebAssembly’라는 이름이 필요했지만, 그 이름은 Wasm에 몇 가지 오해라는 족쇄를 채웠다. 이 글이 그 오해를 쓸어내고 Wasm의 더 또렷한 모습을 보여 주었기를 바란다. Wasm에 흥미가 동했다면 시작할 방법은 아주 많다.
Rust 생태계에는 비브라우저 환경의 대표적인 Wasm 런타임 wasmtime이 있다. wasmtime의 개발 조직인 Bytecode Alliance는 Wasm 도구 모음도 잔뜩 제공한다. 그중에서도 wasm-encoder 크레이트는 컴파일러 제작자에게 특히 유용하다. 바이너리 Wasm 모듈을 인코딩하는 타입 안전한 편리한 API를 제공한다.
Rust 바깥으로 눈을 돌리면, binaryen은 Wasm으로의 컴파일을 쉽고 빠르고 효과적으로 만들어 주는 라이브러리다. C와 자바스크립트 API를 제공한다. 어떤 언어를 쓰든, binaryen은 거의 확실히 FFI 한 끗 차이일 것이다. binaryen 위에 구축된 wasm-opt는 Wasm을 입력 받아 최적화된 Wasm을 돌려주는 CLI를 제공한다.
The WebAssembly Binary Toolkit은 Wasm 작업을 쉽게 해 주는 또 다른 CLI 도구 모음을 제공한다. 이 도구 모음에서 특히 주목할 것은 텍스트와 바이너리 포맷을 서로 변환하는 wasm2wat과 wat2wasm으로, 디버깅에 매우 유용하다.
Wasm을 시작하는 방법은 정말 많다. 개척자 정신이 발동한다면, Wasm의 텍스트 포맷 모양으로 문자열을 쓱쓱 이어 붙여 자바스크립트 API를 통해 로컬 브라우저에서 바로 컴파일해 볼 수도 있다.