WebAssembly란 무엇이며 어디에서 발생했나요?

웹이 문서뿐 아니라 앱을 위한 플랫폼이 된 이후, 일부 고급 앱은 웹브라우저의 한계를 뛰어넘었습니다. 성능 향상을 위해 하위 수준 언어와 상호작용하여 '메탈에 더 근접'하는 접근 방식은 많은 상위 수준 언어에서 볼 수 있습니다. 예를 들어 자바에는 자바 네이티브 인터페이스가 있습니다. JavaScript의 경우 이 하위 수준 언어는 WebAssembly입니다. 이 도움말에서는 어셈블리 언어가 무엇이고 웹에서 어셈블리 언어가 유용한 이유를 알아보고 asm.js의 임시 솔루션을 통해 WebAssembly가 어떻게 만들어졌는지 알아봅니다.

어셈블리 언어

어셈블리 언어로 프로그래밍한 적이 있나요? 컴퓨터 프로그래밍에서 어셈블리 언어는 간단히 어셈블리라고 하며 일반적으로 ASM 또는 asm으로 축약되는 모든 저수준 프로그래밍 언어이며, 언어의 명령과 아키텍처의 기계어 코드 명령 간에 매우 강력한 연관성이 있습니다.

예를 들어 Intel® 64 및 IA-32 아키텍처 (PDF)에서 MUL 명령어 (다중화용)는 첫 번째 피연산자 (대상 피연산자)와 두 번째 피연산자 (소스 피연산자)의 부호 없는 곱셈을 수행하고 대상 피연산자에 결과를 저장합니다. 매우 단순화됩니다. 대상 피연산자는 레지스터 AX에 있는 암시적 피연산자이고, 소스 피연산자는 CX와 같은 범용 레지스터에 있습니다. 결과는 레지스터 AX에 다시 저장됩니다. 다음 x86 코드 예를 살펴보세요.

mov ax, 5  ; Set the value of register AX to 5.
mov cx, 10 ; Set the value of register CX to 10.
mul cx     ; Multiply the value of register AX (5)
           ; and the value of register CX (10), and
           ; store the result in register AX.

비교를 위해 5와 10을 곱하는 목표가 있다면 JavaScript에서 다음과 유사한 코드를 작성할 것입니다.

const factor1 = 5;
const factor2 = 10;
const result = factor1 * factor2;

어셈블리 경로를 사용하면 이러한 하위 수준의 머신에 최적화된 코드가 고수준의 인간 최적화 코드보다 훨씬 더 효율적이라는 것입니다. 앞의 경우에는 문제가 되지 않지만 보다 복잡한 작업의 경우 차이가 클 수 있습니다.

이름에서 알 수 있듯이 x86 코드는 x86 아키텍처에 종속됩니다. 특정 아키텍처에 종속되지 않지만 어셈블리의 성능 이점을 상속하는 어셈블리 코드를 작성하는 방법이 있다면 어떨까요?

asm.js

아키텍처 종속 항목 없이 어셈블리 코드를 작성하는 첫 번째 단계는 asm.js였습니다. 이는 컴파일러를 위한 낮은 수준의 효율적인 타겟 언어로 사용할 수 있는 JavaScript의 엄격한 하위 집합입니다. 이 하위 언어는 C 또는 C++와 같이 메모리에 안전하지 않은 언어를 위한 샌드박스 가상 머신을 효과적으로 보여줍니다. 정적 및 동적 유효성 검사의 조합을 통해 JavaScript 엔진은 유효한 asm.js 코드에 대한 컴파일 최적화 AOT (Ahead-Of-Time) 최적화 전략을 사용할 수 있었습니다. 수동 메모리 관리 (예: C)를 사용하여 정적으로 입력되는 언어로 작성된 코드는 early Emscripten (LLVM에 기반)과 같은 소스 간 컴파일러에 의해 번역되었습니다.

언어 기능을 AOT를 지원하는 언어로 제한하여 성능을 개선했습니다. Firefox 22는 asm.js를 지원한 첫 번째 브라우저이며 OdinMonkey라는 이름으로 출시되었습니다. Chrome에 버전 61에 asm.js 지원이 추가되었습니다. asm.js는 브라우저에서 계속 작동하지만 WebAssembly로 대체되었습니다. 이 시점에서 asm.js를 사용하는 이유는 WebAssembly가 지원되지 않는 브라우저의 대안이 될 수 있습니다.

WebAssembly

WebAssembly는 낮은 수준의 어셈블리 유사 언어로, 네이티브에 가까운 성능으로 실행되고 C/C++, Rust, 그리고 웹에서 실행될 수 있도록 컴파일 타겟과 같은 다양한 언어를 제공합니다. Java 및 Dart와 같은 메모리 관리 언어 지원은 현재 진행 중이며 곧 제공되거나 Kotlin/Wasm의 경우와 마찬가지로 이미 지원되고 있습니다. WebAssembly는 JavaScript와 함께 실행되도록 설계되어 둘 다 함께 작동할 수 있습니다.

WebAssembly 프로그램은 브라우저 외에도 WebAssembly용 모듈식 시스템 인터페이스인 WebAssembly 시스템 인터페이스인 WASI 덕분에 다른 런타임에서도 실행됩니다. WASI는 보안을 유지하고 샌드박스 환경에서 실행할 수 있는 기능을 목표로 여러 운영체제에서 이동할 수 있도록 만들어졌습니다.

WebAssembly 코드 (바이너리 코드, 즉 바이트 코드)는 휴대용 가상 스택 머신 (VM)에서 실행되도록 고안되었습니다. 바이트 코드는 JavaScript보다 빠르게 파싱 및 실행되고 간단한 코드 표현을 보유하도록 설계되었습니다.

명령의 개념적 실행은 명령을 통해 진행되는 기존의 프로그램 카운터를 통해 진행됩니다. 실제로 대부분의 Wasm 엔진은 Wasm 바이트 코드를 기계어 코드로 컴파일한 다음 실행합니다. 안내는 다음 두 가지 카테고리로 분류됩니다.

  • 컨트롤 구성을 형성하고 스택에서 인수 값을 팝하는 제어 명령은 프로그램 카운터를 변경하고 결과 값을 스택으로 푸시할 수 있습니다.
  • 간단한 안내는 스택에서 인수 값을 팝하고 값에 연산자를 적용한 다음 결과 값을 스택으로 푸시한 후 프로그램 카운터를 암시적으로 진행시킵니다.

앞의 예로 돌아가서 보면, 다음 WebAssembly 코드는 이 도움말의 시작 부분의 x86 코드와 같습니다.

i32.const 5  ; Push the integer value 5 onto the stack.
i32.const 10 ; Push the integer value 10 onto the stack.
i32.mul      ; Pop the two most recent items on the stack,
             ; multiply them, and push the result onto the stack.

asm.js는 모두 소프트웨어로 구현됩니다. 즉, 코드를 최적화하지 않더라도 모든 자바스크립트 엔진에서 실행할 수 있지만 WebAssembly에는 모든 브라우저 공급업체가 동의한 새로운 기능이 필요했습니다. 2015년에 발표되고 2017년 3월에 처음 출시된 WebAssembly는 2019년 12월 5일에 W3C 권장사항이 되었습니다. W3C는 모든 주요 브라우저 공급업체 및 기타 이해 관계자의 기여를 통해 표준을 유지합니다. 2017년부터 브라우저는 보편적으로 지원됩니다.

WebAssembly에는 텍스트바이너리라는 두 가지 표현이 있습니다. 위에 보이는 것은 텍스트 표현입니다.

텍스트 표현

텍스트 표현은 S-표현식을 기반으로 하며 일반적으로 파일 확장자 .wat를 사용합니다 (WebAsembly text 형식용). 정말로 원한다면 손으로 작성해도 됩니다. 위의 곱셈 예시를 살펴보고 더 이상 인수를 하드코딩하지 않음으로써 더 유용하게 만들고 있다면 다음 코드를 이해할 수 있을 것입니다.

(module
  (func $mul (param $factor1 i32) (param $factor2 i32) (result i32)
    local.get $factor1
    local.get $factor2
    i32.mul)
  (export "mul" (func $mul))
)

바이너리 표현

파일 확장자 .wasm를 사용하는 바이너리 형식은 사람이 만드는 것은 고사하고 사용자가 사용하기 위한 것이 아닙니다. wat2wasm과 같은 도구를 사용하여 위의 코드를 다음의 바이너리 표현으로 변환할 수 있습니다. (코멘트는 일반적으로 바이너리 표현의 일부가 아니지만 더 나은 이해를 위해 wat2wasm 도구에서 추가되었습니다.)

0000000: 0061 736d                             ; WASM_BINARY_MAGIC
0000004: 0100 0000                             ; WASM_BINARY_VERSION
; section "Type" (1)
0000008: 01                                    ; section code
0000009: 00                                    ; section size (guess)
000000a: 01                                    ; num types
; func type 0
000000b: 60                                    ; func
000000c: 02                                    ; num params
000000d: 7f                                    ; i32
000000e: 7f                                    ; i32
000000f: 01                                    ; num results
0000010: 7f                                    ; i32
0000009: 07                                    ; FIXUP section size
; section "Function" (3)
0000011: 03                                    ; section code
0000012: 00                                    ; section size (guess)
0000013: 01                                    ; num functions
0000014: 00                                    ; function 0 signature index
0000012: 02                                    ; FIXUP section size
; section "Export" (7)
0000015: 07                                    ; section code
0000016: 00                                    ; section size (guess)
0000017: 01                                    ; num exports
0000018: 03                                    ; string length
0000019: 6d75 6c                          mul  ; export name
000001c: 00                                    ; export kind
000001d: 00                                    ; export func index
0000016: 07                                    ; FIXUP section size
; section "Code" (10)
000001e: 0a                                    ; section code
000001f: 00                                    ; section size (guess)
0000020: 01                                    ; num functions
; function body 0
0000021: 00                                    ; func body size (guess)
0000022: 00                                    ; local decl count
0000023: 20                                    ; local.get
0000024: 00                                    ; local index
0000025: 20                                    ; local.get
0000026: 01                                    ; local index
0000027: 6c                                    ; i32.mul
0000028: 0b                                    ; end
0000021: 07                                    ; FIXUP func body size
000001f: 09                                    ; FIXUP section size
; section "name"
0000029: 00                                    ; section code
000002a: 00                                    ; section size (guess)
000002b: 04                                    ; string length
000002c: 6e61 6d65                       name  ; custom section name
0000030: 01                                    ; name subsection type
0000031: 00                                    ; subsection size (guess)
0000032: 01                                    ; num names
0000033: 00                                    ; elem index
0000034: 03                                    ; string length
0000035: 6d75 6c                          mul  ; elem name 0
0000031: 06                                    ; FIXUP subsection size
0000038: 02                                    ; local name type
0000039: 00                                    ; subsection size (guess)
000003a: 01                                    ; num functions
000003b: 00                                    ; function index
000003c: 02                                    ; num locals
000003d: 00                                    ; local index
000003e: 07                                    ; string length
000003f: 6661 6374 6f72 31            factor1  ; local name 0
0000046: 01                                    ; local index
0000047: 07                                    ; string length
0000048: 6661 6374 6f72 32            factor2  ; local name 1
0000039: 15                                    ; FIXUP subsection size
000002a: 24                                    ; FIXUP section size

WebAssembly로 컴파일

보시다시피 .wat.wasm 모두 인간 친화적이지 않습니다. 이때 Emscripten과 같은 컴파일러가 사용됩니다. C 및 C++와 같은 상위 수준 언어에서 컴파일할 수 있습니다. Rust 등 다른 언어를 위한 컴파일러도 있습니다. 다음 C 코드를 살펴보세요.

#include <stdio.h>

int main() {
  printf("Hello World\n");
  return 0;
}

일반적으로 이 C 프로그램은 gcc 컴파일러로 컴파일합니다.

$ gcc hello.c -o hello

Emscripten이 설치된 상태에서 emcc 명령어와 거의 동일한 인수를 사용하여 WebAssembly에 컴파일합니다.

$ emcc hello.c -o hello.html

이렇게 하면 hello.wasm 파일과 HTML 래퍼 파일 hello.html이 만들어집니다. 웹 서버에서 hello.html 파일을 제공하면 "Hello World"가 DevTools 콘솔에 출력됩니다.

HTML 래퍼 없이 WebAssembly로 컴파일하는 방법도 있습니다.

$ emcc hello.c -o hello.js

이전과 마찬가지로 hello.wasm 파일이 생성되지만 이번에는 HTML 래퍼 대신 hello.js 파일이 생성됩니다. 테스트하려면 다음과 같이 Node.js를 사용하여 결과 자바스크립트 파일 hello.js를 실행합니다.

$ node hello.js
Hello World

자세히 알아보기

WebAssembly에 대한 이 간단한 소개는 빙산의 일각에 불과합니다. MDN의 WebAssembly 문서에서 WebAssembly에 대해 자세히 알아보고 Emscripten 문서를 참고하세요. 실제로 WebAssembly를 사용한 작업은 올빼미 밈을 그리는 방법과 비슷한 느낌이 들 수 있습니다. 특히 HTML, CSS 및 JavaScript에 익숙한 웹 개발자는 C와 같은 컴파일될 언어에 정통할 필요는 없기 때문입니다. 다행히도 StackOverflow의 webassembly 태그와 같은 채널이 있습니다. 이러한 채널은 세심하게 질문하면 전문가의 도움을 받을 수 있습니다.

감사의 말

Jakob Kummerow, Derek Schuff, Rachel Andrew가 작성한 도움말입니다.