Go를 사용해 WASM 개발하기
마지막 업데이트:
특징
- 정식 golang 을 사용해 개발하면 모든 기능을 사용할 수 있지만, 컴파일된 바이너리는 GC를 위한 코드가 같이 포함되기 때문에 바이너리 크기가 큰 문제가 있습니다.
- 바이너리의 크기를 줄이려면 정식 golang 에서 일부 기능을 제거한 tinygo 를 사용하는 방법이 있습니다.
- CLI 로 실행하는 WASM 바이너리인 경우에는 바이너리 크기가 별 문제가 안되지만, 웹 브라우저에서 실행되는 경우에는 크기에 민감하기 때문에 최소 1.5 MB ~ 수십 MB의 바이너리를 다운받도록 하는 것은 좋은 선택지가 아니라고 볼 수 있습니다.
개발 환경
- Go 1.17 이상 버전이 설치되어 있어야 합니다.
WASM 모듈 작성
여기서 작성할 WASM 모듈은 스터디 발표 순서를 정하기 위한 제비 뽑기를 하는 함수를 만들어 보겠습니다.
package main
import (
"encoding/json"
"math/rand"
"syscall/js"
"time"
)
func main() {
done := make(chan struct{}, 0)
// drawLots 함수를 자바스크립트 전역 스코프에 wasmDrawLots 라는 이름으로 등록합니다.
js.Global().Set("wasmDrawLots", js.FuncOf(drawLots))
// 채넗을 사용해 main 함수가 종료되지 않도록 합니다.
<-done
}
// drawLots 함수는 1개의 파라미터를 입력값으로 받습니다.
// 파라미터 1:
// - 'Record<string, number>' 형식의 값을 JSON.stringify()를 적용한 값
// - 키: 이름, 값: 가중치
// 반환값: 발표자 이름 (string 타입))
func drawLots(this js.Value, args []js.Value) interface{} {
// dlqfur 파라미터 개수 확인
if len(args) != 1 {
panic("invalid argument count")
}
// 파라미터 1 을 map[string]uint32 형식으로 파싱
jsonArg := args[0].String()
var targets map[string]uint32
if err := json.Unmarshal([]byte(jsonArg), &targets); err != nil {
panic(err)
}
return drawLotsImpl(targets)
}
// ...
JavaScript -> Go 타입 변환
- syscall/js 패키지에 있는 함수와
reflect
패키지를 사용해 JavaScript 타입을 Go 타입으로 변환해야 합니다. - 프리미티브 타입을 제외한 객체타입의 경우, 이를 Go 에서 사용하는 타입으로 변환하기 위한 별도의 함수를 작성하거나 라이브러리를 사용하지 않는다면, 타입 전환이 매우 불편해서 거의 사용할 수 없을 정도입니다.
Go -> JavaScript 타입 변환
- go에서 사용하는 타입은 이 곳에 표시된 규칙에 따라 JavaScript 타입으로 변환됩니다.
빌드
바이너리는 GOOS
, GOARCH
환경변수를 js
, wasm
으로 설정해 빌드합니다.
$ GOOS=js GOARCH=wasm go build -o main.wasm
배포
빌드된 *.wasm
파일을 배포할 때, 빌드한 Go 버전에서 제공하는 wasm_exec.js
파일을 같이 배포해야 합니다.
$ cp -f "$(go env GOROOT)/misc/wasm/wasm_exec.js" ../frontend/
$ cp -f main.wasm ../frontend/
웹 브라우저에서 사용
HTML 파일에서 다음과 같이 wasm_exec.js
파일을 로드해 초기화 작업을 수행합니다. 좀 더 자세한 내용은 https://github.com/golang/go/wiki/WebAssembly#getting-started 에서 확인할 수 있습니다.
<!DOCTYPE html>
<html lang="ko-KR">
<head>
<title>WASM</title>
<meta charset="utf-8" />
<script src="wasm_exec.js"></script>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
const wasm = result.instance;
go.run(wasm);
});
</script>
</head>
<body>
<button onclick="onRun()">Run</button>
<script>
function onRun() {
const result = wasmDrawLots(JSON.stringify({
"foo": 10,
"bar": 10,
"foobar": 10,
}));
console.log("result:", result);
}
</script>
</body>
</html>
- Go 에서
wasmDrawLots
이라는 이름으로 전역 함수를 등록했기 때문에, 바로 호출할 수 있습니다. - 반환값을 콘솔에 출력해서 제대로 호출되었는지 확인합니다.
실행방법
위의 HTML 파일이 동작화도록 하려면 웹 서버를 띄운 상태에서 index.html
파일을 열어야 합니다.
여기서는 간단하게 파이썬에 내장되어 있는 HTTP 서버를 사용해 실행해보겠습니다.
# wasm 빌드 -> html 디렉토리에 배포
$ task dist-go
$ cd html
# HTTP 서버 실행
$ python3 -m http.server
- http://localhost:8000 접속
- 개발자 도구 콘솔 창 표시
- “Run” 버튼을 눌러 결과가 표시되는지 확인