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

웹이 문서뿐만 아니라 앱을 위한 플랫폼이 된 이래로, 최첨단 앱 중 일부는 웹브라우저의 한계를 뛰어넘었습니다. 성능을 개선하기 위해 하위 수준의 언어와 상호작용함으로써 '메탈에 더 가까이' 접근하는 접근 방식은 많은 상위 수준 언어에서 찾아볼 수 있습니다. 예를 들어 Java에는 Java 네이티브 인터페이스가 있습니다. 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)를 통해 정적으로 형식이 지정된 언어로 작성된 코드는 초기 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는 자바스크립트와 함께 실행되도록 설계되었기 때문에 둘 다 함께 작동할 수 있습니다.

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-표현식을 기반으로 하며 일반적으로 WebAssembly text 형식의 경우 .wat 파일 확장자를 사용합니다. 원하는 경우 직접 작성할 수도 있습니다. 위의 곱셈 예시를 더 이상 하드코딩하지 않고 더 유용하게 만들면 다음 코드를 이해할 수 있습니다.

(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;
}

일반적으로 gcc 컴파일러를 사용하여 이 C 프로그램을 컴파일합니다.

$ 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 태그와 같은 채널은 정중하게 요청하면 전문가가 도움을 줄 수 있는 경우가 많습니다.

감사의 말씀

이 도움말은 Jakob Kummerow, Derek Schuff, Rachel Andrew가 검토했습니다.