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

웹이 문서뿐만 아니라 앱을 위한 플랫폼이 된 이래로, 최첨단 앱 중 일부는 웹브라우저의 한계를 뛰어넘었습니다. 성능을 개선하기 위해 하위 수준 언어와 상호작용함으로써 '메탈에 더 가까이' 접근하는 접근 방식은 많은 상위 수준 언어에서 등장합니다. 예를 들어 Java에는 Java 네이티브 인터페이스가 있습니다. JavaScript의 경우 이 하위 수준 언어는 WebAssembly입니다. 이 도움말에서는 어셈블리 언어가 무엇인지, 웹에서 유용할 수 있는 이유를 알아본 후 asm.js의 임시 솔루션을 통해 WebAssembly가 생성된 방법을 알아봅니다.

어셈블리 언어

어셈블리 언어로 프로그래밍한 적이 있나요? 컴퓨터 프로그래밍에서 어셈블리 언어(단순히 어셈블리라고도 하며 일반적으로 ASM 또는 asm으로 약칭)는 언어의 명령어와 아키텍처의 머신 코드 명령어 간에 매우 강한 상관성이 있는 모든 저수준 프로그래밍 언어입니다.

예를 들어 Intel® 64 및 IA-32 아키텍처 (PDF)를 보면 MUL 명령어 (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

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

언어 기능을 AOT에 적합한 기능으로 제한하여 성능이 개선되었습니다. Firefox 22는 OdinMonkey라는 이름으로 출시된 최초의 asm.js를 지원한 브라우저입니다. Chrome 버전 61에서 asm.js 지원이 추가되었습니다. asm.js는 여전히 브라우저에서 작동하지만 WebAssembly로 대체되었습니다. 현재 asm.js를 사용하는 이유는 WebAssembly를 지원하지 않는 브라우저의 대안으로 사용하기 위해서입니다.

WebAssembly

WebAssembly는 거의 네이티브 성능으로 실행되고 C/C++ 및 Rust와 같은 언어를 제공하며 컴파일 타겟이 포함되어 웹에서 실행되도록 하는 등 컴팩트한 바이너리 형식의 하위 수준 어셈블리와 유사한 언어입니다. Java, Dart와 같은 메모리 관리 언어 지원 및 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는 모두 소프트웨어로 구현되지만, 최적화되지 않은 경우에도 코드가 모든 JavaScript 엔진에서 실행될 수 있지만 WebAssembly는 모든 브라우저 공급업체가 동의한 새로운 기능이 필요했습니다. 2015년에 발표되었으며 2017년 3월에 처음 출시된 WebAssembly는 2019년 12월 5일에 W3C 권장사항이 되었습니다. W3C는 모든 주요 브라우저 공급업체와 기타 이해관계자의 참여를 통해 표준을 유지합니다. 2017년부터 브라우저 지원은 보편적입니다.

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

텍스트 표현

텍스트 표현은 S 표현식을 기반으로 하며 일반적으로 파일 확장자 .wat (WebAssembly 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 파일을 제공하면 DevTools 콘솔에 "Hello World"가 출력됩니다.

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

$ emcc hello.c -o hello.js

이전과 마찬가지로 hello.wasm 파일이 생성되지만 이번에는 HTML 래퍼 대신 hello.js 파일이 생성됩니다. 테스트하려면 결과 JavaScript 파일 hello.js를 Node.js와 함께 실행합니다.

$ node hello.js
Hello World

자세히 알아보기

WebAssembly에 대한 이 간단한 소개는 빙산의 일각에 불과합니다. MDN의 WebAssembly 문서에서 WebAssembly에 대해 자세히 알아보고 Emscripten 문서를 참고하세요. 사실 WebAssembly로 작업하는 것은 올빼미 그리기 방법 밈과 약간 비슷할 수 있습니다. 특히 HTML, CSS, JavaScript에 익숙한 웹 개발자가 C와 같이 컴파일할 언어에 능숙하지 않을 수 있기 때문입니다. 다행히 StackOverflow의 webassembly 태그와 같은 채널이 있으므로 전문가에게 정중하게 요청하면 기꺼이 도와주기도 합니다.

감사의 말씀

이 도움말은 제이콥 쿰메로우, 데릭 슈프, 레이첼 앤드류가 검토했습니다.