제목 : 웹 어셈블리와 Node.js
Node.js 에서 싱글스레드 기반으로 운용되는 Node.js 환경을 멀티스레딩 환경으로 만들어,
CPU 집약 비즈니스를 나눌 수 있는 방식을 탐색 했다.
이후, 더 최적화 할 수 있는 방식을 연구하며
Web Assembly 라는 것을 알게 되었다. 이 글은 밑의 블로그 포스팅과 이어진다.
https://codecreature.tistory.com/201
갑자기 왜 어셈블리?
Node.js 는 현재 웹 서버와 웹 어플리케이션 서버에서 주요하게 사용되고 있다.
하지만, 아무리 CPU 집약 비즈니스 코드를 나누어 스레드로 새로 생성하더라도,
새로운 스레드 또한 JavaScript 코드를 해석하기 위해 Node.js 엔진을 사용한다.
물론, V8 엔진도 훌륭하지만, 기계어가 가깝게 이미 파싱되어 있는 코드보다는 성능이 떨어 질 수 밖에 없다.
여기서 실제 웹과, 백엔드에서 사용할 수 있는 "웹 어셈블리" 라는 개념이 등장한다.
실제 어셈블리 코드라기보다는, 웹을 위한 어셈블리코드라는 개념으로 "Web Assembly" 라고 부른다.
여기서 Web Assembly 의 파일 형태를 2 개로 나뉘는데,
.wasm
: 이진 형태의 모듈.wat
: 웹 어셈블리의 코드 텍스트를 가지고 있는 모듈
로 나뉜다.
또한, 웹 어셈블리는 다양한 언어로부터 컴파일 될 수 있다.
- C, C++
- Rust
- Go
- AssemblyScript (TypeScript 와 닮은 문법 But 인지도는 낮은듯)
- ...
-여기서 잠깐 드는 생각-
Node.js 에서 웹 어셈블리를 사용 할 정도가 되면, 최적화가 많이 필요하다는 것인데,
어셈블리 모듈을 사용하여 최적화를 할 정도면, 백엔드 프레임워크와 언어 자체를 바꾸는게 맞지 않나? 생각이 든다.
그래도 NestJS 를 공부하여 팀 프로젝트로 백엔드를 구축 해 봤으므로,
이미 NestJS 를 이용하여 의존성 모듈 구성 및 프로젝트를 완성했다면, 이러한 기능 또한 필요할 것이다.
새로운 언어와 프레임워크로 바꾸면서 드는 비용과, 웹 어셈블리를 지속적으로 도입하여 투자하는 비용.
두 개를 비교했을 때, 어느 한 쪽이 항상 우세하다고 판단하기는 어렵기 때문이다.
어떤 언어로 웹 어셈블리를 구성하면 좋을까?
이건 현재 Node.js 를 배우는 사람에게 달린 문제인 것 같다.
C 혹은 C++ 을 배운 적이 있다면, C 혹은 C++ 로 하면 된다.
그리고, Rust 를 공부했다면, Rust 로 하면 된다.
혹시라도 Node.js 를 먼저 접해 다른 언어를 배워본 적 없다면, AssemblyScript 를 사용하면 된다.
JavaScript 를 사용하면서 여기까지 구현하고자 한다면, 정말 의지가 대단한 사람이라고 칭찬하고 싶다..
사실, JavaScript 는 입문은 매우 간단하지만, C 나 C++ 보다도 훨씬 어렵다고 생각이 든다.
쉽게 사칙연산을 수행하거나, 함수를 선언 할 수 있으며, 자유도가 높은 것은 당연히 JavaScript 일 것이다.
하지만, 신뢰성과 최적화를 수행하기는 가장 어려운 언어가 아닌가 생각이 든다.
하지만, 유연한 커뮤니티와 거대한 의존성 레지스트리를 가지고 있는 Node.js 환경에서
쉽게 다른 언어로 이전하기 어렵다는 것 또한 인지하고 있다.
따라서, 이번 예제는 웹 어셈블리 변환을 지원하는 언어 중,
가장 타입스크립트와 유사해 보이는 AssemblyScript 를 사용 해 볼 것이다.
일단, 우리가 기존에 사용하던 타입이 약간 다르다는 게 보인다.
공식 페이지 가이드 코드
export function fib(n: i32): i32 {
var a = 0, b = 1
if (n > 0) {
while (--n) {
let t = a + b
a = b
b = t
}
return b
}
return a
}
위의 코드는 간단한 피보나치 수열 계산 함수이다.
다행히 백준에서 10억번째 피보나치 수를 N 으로 나누었을 때의 나머지는 얼마인가에 대해 풀어 본적이 있어서
위의 코드를 쉽게 이해 할 수 있었다. 행렬을 이용해야 했다..
잡설은 그만두고, 이 파일을 그대로 타입스크립트 파일로 넣어보면, i32
타입이 인식되지 않는다.
이는, AssemblyScript 를 ts 로 인식하기 위한 프로젝트 설정을 해 주지 않았기 때문이다.
현재 코드 예제들을 tsconfig.json
이 존재하는 장소에 저장하고 있는데,
이번에는 새로운 디렉토리를 생성하여, .ts
확장자가 AssemblyScript
로 인식하도록 만들어 보겠다.
1. 디렉토리 따로 생성
$ mkdir web-asm-exam
$ cd web-asm-exam
$ npm init --y # web-asm-exam 디렉토리에 "package.json" 파일 생성됨.
2. assemblyscript 를 개발 의존성으로 설치
$ npm install --save-dev assemblyscript # "devDependencies" 에 assemblyscript 가 들어간다.
어떤 개발 의존성들이 설치되었을까 궁금해서 package-lock.json
을 뜯어봤다.
보니, 2 개의 연관 의존성들이 추가로 설치되었다.
assemblyscript
binaryen
: AssemblyScript 를 위한 특별 타입 지원long
: 64 비트 수준의 숫자 안정성 지원
3. AssemblyScript 컴파일러 설치
$ npx asinit .
명령어 이후에 어셈블리스크립트를 지원하기 위한 다양한 폴더들이 생성된다.
Ex
➜ web-asm-exam tree -L 2
.
├── asconfig.json
├── assembly
│ ├── index.ts
│ └── tsconfig.json
├── build
├── index.html
├── node_modules
│ ├── assemblyscript
│ ├── binaryen
│ └── long
├── package-lock.json
├── package.json
└── tests
└── index.js
내부적으로 설치되는 node_modules
가 있어, 2 단계까지만 간단히 보이도록 했다.
보다시피, 구성 파일과 디렉토리 구조를 생성한다.
그리고, 파일을 작성하는 디렉토리는 <asm 프로젝트>/assembly
이다.
여기에 기본적인 예시가 담겨진 파일 index.ts
가 있다.
// The entry file of your WebAssembly module.
export function add(a: i32, b: i32): i32 {
return a + b;
}
이 파일을 .wasm
으로 빌드하기 위해서는,
프로젝트 root 위치에서,
$ npm run asbuild
> web-asm-exam@1.0.0 asbuild
> npm run asbuild:debug && npm run asbuild:release
> web-asm-exam@1.0.0 asbuild:debug
> asc assembly/index.ts --target debug
> web-asm-exam@1.0.0 asbuild:release
> asc assembly/index.ts --target release
이러한 로그 결과물을 남긴다.
그리고, <프로젝트 root>/build/..
에 결과물이 생긴다.
➜ build tree
.
├── debug.d.ts
├── debug.js
├── debug.wasm
├── debug.wasm.map
├── debug.wat
├── release.d.ts
├── release.js
├── release.wasm
├── release.wasm.map
└── release.wat
1 directory, 10 files
여기서 degug
를 가지고 있는 파일은 npm test
를 통해 실행되는 디버깅 파일들이다.
release
이름을 가지고 있는 파일들이 실제 실행 파일들이다.
하지만, 굳이 전문적으로 AssemblyScript
를 배울 필요는 없다 생각하여,
release.wasm
만 빼서 넣기로 결정했다.
.wat
, .wasm
이 npm run asbuild
를 통해 나온 최종 결과물이다.
.wasm 파일을 node 환경에서 실행하는 법
먼저, node.js 환경에서 이진 파일인 .wasm
을 곧바로 사용할 수는 없다.
.wasm
파일을 사용하기 위해, 3 가지 단계를 지난다.
- 파일을 읽어와 Node.js 엔진의 Buffer 에 담는다.
- Node.js 전역객체인
WebAssembly
를 이용하여, 버퍼에 담긴 파일을 인스턴스화 한다. - 전달된 어셈블리 모듈을 이용하여 JS 로 이용한다.
먼저, .wasm
파일과 .ts
or .js
파일이 공존할 디렉토리를 만들거나, 지정한다.
해당 디렉토리에 빌드된 .wasm
과, ts
or js
파일을 넣는다.
내가 빌드한 웹 어셈블리 파일은 add
함수를 내보내기 때문에,
이름을 add.wasm
으로 바꾸었다.
import * as fs from "fs"
import * as path from 'path';
// 파일을 자체적으로 가져와 버퍼로 이동
const wasmBuffer : Buffer = fs.readFileSync(path.join(__dirname, "./add.wasm"));
// 인스턴스화 된 모듈은 어떠한 형태인지 짐작 할 수 없다. 하지만, 우리는 이미 .wasm 파일이 어떤 함수나 값을 방출하는지 알기에, 인터페이스로 알려준다.
interface AddExports {
add(a : number, b : number) : number;
}
// 모듈 초기화 및 함수 구성
async function init (buffer : Buffer) {
// 단순한 buffer 데이터를 Node.js 에서 사용할 수 있는 형태로 가공한다.
const wasmModule = await WebAssembly.instantiate(buffer);
/*
곧바로 as AddExports 하면 생기는 에러는 :
Conversion of type Exports to type AddExports may be a mistake because neither type sufficiently overlaps with the other.
If this was intentional, convert the expression to unknown first.
이 뜻은, 내가 적용한 AddExports 인터페이스가, exports 유형으로 선언된 (내부 파일) type ExportValue = Function | Global | Memory | Table;
위의 유형들과 겹치지 않기 때문이다.
따라서, 이것이 만약 의도 된 행동이라면, 표현식을 unknown 으로 변환하라는 것이다.
이에 따라 나는 as unknown 으로 래핑하고, 다시 AddExports 타입으로 래핑했다.
*/
const {add} = wasmModule.instance.exports as unknown as AddExports;
// Promise 로 감싸진 어셈블리 함수 add 를 내보낸다.
return {add};
}
// Promise 로 감싸진 어셈블리 모듈을 가져온다.
const addWasmModule = init(wasmBuffer);
// return 되는 결과물에는 이제 우리가 찾던 함수 "add" 가 존재한다. 이를 이용하여 계산한다.
addWasmModule.then((moduleExports) => {
const result = moduleExports.add(3, 5);
//
console.log(result);
})
Result :
➜ worker-exam tsc wasm-js-exam-1.ts
➜ worker-exam node wasm-js-exam-1.js
8
이 내용을 탐구하고 나서 드는 생각.
각각의 고유한 언어에는 Learning Curve 라는 것이 존재한다.
처음 각각의 언어를 접했을 때, 첫 인상은 당연히 다를 수 밖에 없다.
JavaScript 와, SuperSet 인 TypeScript 와의 결합은 쉽게 일반인들도 입문할 수 있게 만들어졌다고 생각한다.
그러나, JavaScript 의 클래스 선언은 "문법적 설탕" 이라고 부르는 것이었다.
물론, JS 의 모든 것이 Function
객체는 아니지만, 결국 Function
객체로 통한다는 것이다.
JavaScript 최신 문법을 도입하기 위한 Babel 의 결과물은 다시 JavaScript 였다.
JavaScript 에서 개발 중 타입 안정성을 구현하기 위해 개발된 TypeScript 조차, JS 로 컴파일된다.
나는 단순히 "실행된다" 에 멈추지 않고, "어떻게 컴파일 되었는가" 에 집중했다.
이는 내가 소프트웨어 엔지니어로서 조금 더 성숙해졌다는 증거이기도 했다.
하지만, 나는 Node.js 를 실행하기 위해 사용되는 JS 문법의 난해함과,
오히려 JS 를 깊이 사용하기 위해 결국은 최적화 된 언어를 사용해야 한다는 회의감이 들었다.
이 글을 작성 한 이유는, 모두가 Node.js 환경에서 다른 여타 언어들 (Go, Java, Rust 등) 보다 느리다는 것을 알고,
Node.js 환경에서의 한계를 극복하려고 나 나름대로 노력한 것이다.
그런데, 결국은 JS 를 더 파고들수록, 최신 모던 프로그래밍 언어의 트렌드를 따라가고,
이를 단순히 JS 로 적용하기 위해서만 만들어졌다는 심증을 없애기는 무리였다.
사실, 나는 지금 NestJS 의 커스텀 데코레이터와 더불어, 성숙도를 높이려고 노력하고 있었다.
하지만, 나는 흔들리는 중이다.
최신 마이크로 소프트에서 TypeScript 를 컴파일하는 데 있어 느린 속도를 개선하기 위해
Go 를 도입했다는 소식을 들었다. 7.0 버전부터 적용될 것이다.
허나, 나는 다른 생각이 들기 시작했다.
물론 TypeScript 를 번역하기 위해 다시 TypeScript 를 사용하는 것은, 비효율적인 과정일 수 있다.
하지만, TypeScript 를 번역하기 위해 GO 를 사용한다는 것은, 결국 TypeScript 의 역할,
즉, 결국 JS 의 역할을 Go 가 10 배 더 빠른 효율로 제공한다는 것이 아닌가?
물론, 최신 브라우저가 사용하고 있는 V8 엔진 성능의 훌륭함과,
더불어 javascript 도 효율적으로 사용 될 수 있다는 것을 안다.
그리고, JS 와 TS 의 커뮤니티 활동성도 굉장하다는 것을 알고 있다.
나는 구현하고자 하는 사람인가? 아니면 탐구하고자 하는 사람인가?
위에 대한 대답은 조만간 이루어 질 것 이라고 생각한다.
참고 사이트 - 새 창으로 열림
https://webassembly.org/getting-started/js-api/
'Node.js > 잡다 지식' 카테고리의 다른 글
비밀번호는 왜 해싱할까? - With Node.js (0) | 2025.04.15 |
---|---|
NestJS 의 Interceptor 는 무엇일까? - (NestInterceptor) (0) | 2025.04.12 |
node.js 로 멀티 스레드 구현하기 (Worker) (0) | 2025.04.07 |
package.json 과 package-lock.json 은 무엇일까? (0) | 2025.04.03 |
JavaScript, TypeScript 에서 진짜 Private 구현하기 (1) | 2025.03.31 |