Wasm 간단 사용기

marong61
WATCHA
Published in
11 min readJun 23, 2021

--

2015년에 WebAssembly(이하 wasm)가 탄생하고 2017년에 주요 브라우저에서 wasm 을 사용할 수 있게 된 이후로 다양한 방법으로 wasm 가 사용되고 있습니다.

대부분의 웹 프론트 개발자가 wasm 에 흥미를 느끼는 이유는 빠른 성능 때문일 거라고 생각합니다. 저 또한 그러한데요. 그래서 wasm 를 어떻게 만들어야 할까? 어떻게 적용해야 할까? 하는 생각을 어렴풋이 하고 있었습니다.

어떻게든 한번 만들어 봐야 감이 올 거 같아서, wasm 을 컴파일할 수 있는 언어를 먼저 배워야겠다고 생각했죠. 그 와중에 Rust가 눈에 들어왔고, 간단한 Rust 코드를 wasm 로 컴파일해 웹 브라우저에서 구동 시켜 보았습니다.

지금부터 그 경험을 공유하려 합니다.

먼저 wasm와 Rust가 익숙하지 않은 사람들을 위해 간단히 소개하려 합니다. 물론 간단 “소개”이기 때문에 빙산의 일각보다 더 작은 부분을 설명하고 있습니다. 흥미가 있으신 분들은 인터넷에서 더 다양한 자료를 접하며 공부하시길 바랍니다. (저도 Rust,wasm 를 처음 접한 초심자입니다. 함께해요!)

Wasm 란?

WebAssembly는 최신 웹 브라우저에서 실행할 수 있는 새로운 유형의 코드이며 새로운 기능과 성능 면에서 큰 이점을 제공합니다.

라고 MDN에 정의되어 있습니다.

문서를 좀 더 자세히 읽어보면, C,C++,Rust 등과 같은 언어의 컴파일 타깃이며, 이런 여러 언어로 작성된 프로그램을 웹에서 아주 빠른 속도로 실행할 수 있도록 해주는 일종의 도구라고 볼 수 있습니다.

Wasm 는 왜 빠른가?

호다다다다다닥

위의 설명대로 wasm는 웹에서 일반적으로 아주 빠른 속도로 실행되어 성능상에 이점이 많습니다. 어떻게 wasm는 그렇게 빠르게 작동할 수 있을까요?

첫째로 wasm 는 바이너리 파일로 동일한 조건의 javascript 코드 보다 코드의 크기가 작기 때문에 모듈로서 로딩하는데 걸리는 시간이 더 적습니다.

두 번째로 wasm 이미 기계어에 가깝게 컴파일되기 때문에 javascript 가 브라우저에서 동작하기 위한 과정들, 파싱, 컴파일, 최적화 등의 과정을 거의 거치지 않고 실행되기 때문에 아주 빠른 속도로 실행될 수 있습니다.

이렇듯 wasm는 개발자를 흥미롭게 하는 부분들이 많은데요. wasm 를 만들 수 있는 다양한 언어 중 저는 Rust 를 이용해 wasm 를 만들어 봤습니다.

Rust 란?

A language empowering everyone to build reliable and efficient software.

제가 느낀 Rust 는 javascript 와 꽤 다른 쪽에 서 있는 언어였습니다.

물론 TypeScript 가 나온 이후, 저를 포함한 많은 웹 프런트 개발자들이 타입을 정의하는 것에 큰 어색함은 없어졌지만, Rust 는 TypeScript 보다 엄격한 Type System을 가지고 있었고, javascript 와는 다른 컴파일언어, 그리고 바이너리 실행 파일을 결과로 내놓는 오랜만에 접해본 저수준 언어였습니다.

또한 Rust 는 소유권이라는 독특한 시스템으로 메모리를 유지했습니다.

Rust 의 소유권은 이 글에서 설명하기엔 꽤 큰 개념이기 때문에 관심이 있는 분들은 아래 링크를 통해 깊게 공부해보세요!

https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html

Rust 가 wasm 와 꽤 잘 어울리는 이유?

Rust 는 저수준 언어로 런타임이 매우 적습니다. 여기서 말한 런타임은 흔히 코드가 실행되는 시간을 의미하는 런타임이 아닙니다.

위의 런타임은 언어가 실행될 때 바이너리에 추가되는 코드를 말합니다. 이 코드는 언어마다 크기의 차이가 있으며, Rust 는 런타임이 거의 없어 컴파일 이후 생성되는 바이너리의 크기가 매우 작습니다.

따라서 Rust 로 wasm 를 만들면 그 크기가 매우 저렴합니다.

또 Rust 는 GC 가 없습니다. 그렇지만 c 와 같은 alloc, dealloc 또한 프로그래머가 직접 하지 않습니다. (할 수 있는 방법은 있습니다)

그런데도 Rust 의 메모리 관리는 안정성이 매우 높습니다. 이 장점은 wasm 으로 컴파일되어도 그대로 유지됩니다.

그래서 Rust 로 어떻게 wasm 를 만드는 건데?

어떻게 만드러?

Rust 로 wasm 을 만들어 내는 방법은 몇 가지 있습니다. 그런데 wasm-pack, wasm-bindgen crate 들을 이용하는 방법이 제일 문서화가 잘 되어있고, MDN 에서도 소개하고 있는 방법입니다. (crate는 rust 가 만들어낸 바이너리 파일인데, 쉽게 말하면 라이브러리라고 생각하면 됩니다)

wasm-pack은 Rust 코드들을 쉽게 wasm 로 build 하고 testing 할 수 있는 도구입니다.

wasm-bindgen은 Rust 와 javascript 간의 상호작용을 쉽게 하기 위한 도구입니다. 예를 들면 Rust 클래스를 javascript 에서 사용할 수 있고, javascript string 을 Rust 함수의 입력으로 줄 수 있습니다.

wasm의 리턴값을 javascript 에게 전달하려면 타입이 i32, i64, f32, f64여야 합니다. 만약 string 을 리턴값으로 주고싶다면 string 이라는 타입을 주면 안 되고 wasm 내에 string 이 시작되는 주소 값과, 길이를 javascript 에게 알려줘야 하며 javascript 는 그 주소와 길이를 가지고 ArrayBuffer 를 통해 string 으로 읽어야 합니다.

wasm-bindgen 은 위의 불편함을 javascript background 를 사용하여 우회합니다. wasm-bindgen 을 이용하여 wasm 을 빌드하면 blah.bg.js 라는 파일이 wasm 파일과 함께 나오게 됩니다.

아래 그림으로 간단한 도식을 나타내 보았습니다.

만약 내가 짠 Rust 코드중 string 이라던지 class 라던지 복잡한 데이터형을 javascript 와 상호작용하고자 한다면 wasm-bindgen 은 javascript background 파일을 함께 output 으로 내놓아 그 과정을 해결합니다.

아래 코드를 보고 과정을 이해해 봅시다.

위의 Rust 코드를 wasm-pack 을 통해 wasm 로 컴파일하면 아래와 같은 background javascript 가 생성됩니다.

6 line을 보면 return_integer 를 볼 수 있는데, 단순히 wasm 의 함수를 실행하고 있습니다.

그에 비해 39 line부터 시작되는 return_str 은 꽤 복잡하게 이루어져 있습니다. 코드를 좀 더 자세히 보면 wasm로 부터 메모리 주소를 받아와 주소를 wasm 함수로 전달, 그 후 주소 값부터 특정 길이를 읽어 javascript string 을 반환하게 되고, 마지막으로 불필요한 wasm 메모리를 해제해줍니다.

이런 일련의 과정들을 wasm-bindgen 이 해주는 것인데요. 만약 wasm-bindgen 을 사용하지 않는다면, 이런 과정들을 스스로 구현해 string 과 같은 복잡한 타입을 읽어내는 javascript 코드와 메모리를 관리하는 코드가 필요합니다.

이러한 불편함을 해결하기 위해 interface-types가 제안된 상태입니다. wasm-bindgen 팀은 interface-types 가 채택되면 javascript background 가 점점 줄어들 것이라고 얘기하고 있습니다.

wasm는 실제로 빠를까?

그럼 실제로 wasm로 된 코드는 얼마나 빠를까요? javascript 와의 비교를 위해 동일한 로직을 wasm 로 바꿔 성능 테스트를 해보았습니다.

왓챠에서 지난겨울 특정 페이지에 들어가면 눈이 내리는 효과를 줬는데요. 아래 코드가 있습니다.

canvas 를 이용한 애니메이션이며, requestAnimationFrame 을 이용하여 프레임마다 처음 생성한 눈 입자들위치를 변경해주는 방식으로 구현되었습니다.

여기서 특별히 성능에 영향을 미치는 곳은 프레임마다 입자를 계산하는 구간입니다. 따라서 그 구간을 Rust 로 만든 wasm 로 대체하였습니다. 아래는 Rust 코드입니다.

javascript 로 구현된 update 를 Rust 로 변경하고, 쉽게 입자들의 위치를 알 수 있도록 메소드를 구현했습니다. 입자들의 위치를 반환하는 메소드는 여러 방식으로 구현할 수 있습니다만 javascript background 가 가벼워지도록 index 를 받아 x,y,d,rf32 타입으로 하나씩 반환하도록 했습니다.

그럼 이제 실제로 퍼포먼스 측정해 보겠습니다.

snow.js의 line51을 보면 performanceChecker 라는 함수에 update함수와 dom element 를 파라미터로 전달하고 있습니다.

아래 코드가 있습니다.

단순히 파라미터로 받은 함수를 실행하고, 그 실행 전,후에 performance.now() 를 계산해 dom 에 width 로 반영하는 함수입니다.

50000개의 눈입자

파란색 width 가 javascript 의 performance 를 나타낸 바 이며, 빨간색이 wasm 으로 구현한 update 함수의 performance 를 나타낸 바 입니다.

입자의 개수가 적을 땐 많은 차이가 없지만, 입자를 늘리면 늘릴수록 차이가 더 발생하는데요. 단순 계산도 많은 양을 계산하는 경우엔 wasm가 성능이 더 우월하다는 것을 알 수 있습니다.

이렇게 wasm 을 단순하게 구현해 성능을 비교해봤습니다. 성능은 같은 언어라 할지라도 어떻게 짜느냐에 따라 많이 달라질 수 있습니다. 따라서 더 좋은 wasm 을 만드는 방법이 있을 수도 있고, 더 효율적으로 Rust 를 이용해 wasm 을 만들어 내는 방법도 있을 수 있습니다.

좀더 최적화가 잘 된 코드라면 javascript 와 성능 차이가 더 날 수도 있고, 최적화가 안좋게 된 코드라면 javascript 와 별 차이가 없을 수도 있을 거 같습니다.

공부하면서 참고한 wasm 와 rust 에 관련된 글을 참고하며 글을 마칩니다.

여러분도 한번 경험해 보세요!

wasm 참고 링크

Rust 참고 링크

Rust 공부 링크

위의 링크로 Rust 를 경험 후 흥미가 생긴다면 공식 자습서를 통해 더 깊이 배우는 것을 추천합니다!

--

--