Gleam 프로젝트를 다양한 방식으로 실행 파일로 패키징하는 방법과 각 방법의 장단점 및 주의사항을 정리한 가이드.
Gleam은 비교적 새로운 함수형 프로그래밍 언어로, Erlang과 JavaScript로 컴파일됩니다. 익숙한 Rust 스타일의 문법을 갖추고 있으면서도 복잡도는 Elm과 비슷하며, 전반적으로 작업하기에 매우 즐거운 언어입니다.
하지만 문제는 Gleam이 실행 파일 생성(Executables)을 네이티브로 지원하지 않는다는 점입니다.
이 노트/가이드는 Gleam 실행 파일을 다양한 방식으로 만드는 방법을, 각 방식의 장점과 주의사항과 함께 설명합니다.
가이드 전반에 걸쳐 실행 파일 생성 과정을 보여주기 위해, 진행 중인 WIP project를 사용하겠습니다.
gleam new <project_name>으로 새 프로젝트를 생성합니다.gleam build --target=erlang|javascript로 프로젝트를 빌드합니다.타깃은 중요합니다. 빌드 명령이 생성하는 출력물이 달라지고, 그에 따라 코드를 실행 파일로 패키징하는 방식도 달라지기 때문입니다.
Gleescript는 Gleam 프로젝트로부터 단일 실행 파일을 만들 수 있게 해주는 도구입니다. Erlang escript stdlib module을 사용해 escript를 생성하며, 이렇게 생성된 escript는 Erlang VM에서 실행할 수 있습니다.
단점은 대상 머신에 Erlang VM이 설치되어 있어야 한다는 점입니다.
공식 문서의 인용은 다음과 같습니다 -
The escript can run on any computer that has the Erlang VM installed. Older versions of the virtual machine may not support the newer bytecode contained in the escript. Typically being within a couple major versions of the version used to build the escript is safe.
gleam add gleescript로 gleescript를 의존성으로 추가합니다.gleam build --target erlang로 프로젝트를 빌드합니다.gleam run -m gleescript로 escript를 생성합니다../your_project로 실행 파일을 실행합니다.Burrito는 Elixir 애플리케이션을 BEAM _burrito_로 감싸는 도구라고 할 수 있습니다. gleescript와 달리, 호스트 머신에 Erlang VM이 설치되어 있을 필요가 없습니다. 공식 문서의 인용은 다음과 같습니다 -
Builds a self-extracting archive for a Mix project, targeting Windows, MacOS, and Linux, containing:
- Your compiled BEAM code
- The required ERTS for your project
- Compilation artifacts for any elixir-make based NIFs used by the project
Burrito를 Gleam에서 동작하게 만들 만큼 충분히 실험해 보지는 못했지만, Elixir와 Erlang 프로젝트를 지원하므로 시도해 볼 가치는 분명히 있습니다. 아마도 프로젝트를 escript로 변환한 다음 Burrito로 감싸서, 자체 포함(self-contained) 실행 파일을 만들 수 있을지도 모릅니다.
Deno compile은 Deno에 내장된 명령으로, JavaScript 파일을 단일 실행 파일로 컴파일할 수 있게 해줍니다. 실행 파일 안에 경량 Deno 런타임이 함께 번들되므로, Deno가 설치되어 있지 않은 시스템에서도 실행할 수 있습니다.
여전히 Webpack/Parcel/Rollup/Esbuild 같은 번들러를 사용해 생성된 Gleam 코드를 단일 파일로 번들해야 합니다. Deno는 예전에 deno bundle로 애플리케이션 번들을 지원했지만, 이는 앞서 언급한 다른 번들러들을 사용하는 방향으로 deprecated 되었습니다.
gleam build --target=javascript로 Gleam 프로젝트를 빌드합니다.
번들러로 생성된 JavaScript 파일들을 단일 파일로 번들합니다(여기서는 ESbuild를 사용합니다) esbuild build/dev/javascript/<project_name>/<project_name>.mjs --platform=node --minify-whitespace --minify-syntax --bundle --outfile=bundle.cjs --format=cjs --footer:js=\"main();\"
node로 지정합니다. 제 프로젝트는 내부적으로 Node.js API를 사용하고 있기 때문입니다(Gleam simplifile package).bundle.cjs로, 포맷을 cjs(CommonJS)로 지정합니다. 이는 제가 먼저 Node SEA(다음 방법)를 시도한 결과이지만, Deno는 CommonJS만 지원한다고 명시적으로 선언하지는 않았으니 ESM도 아마 동작할 것입니다.main 메서드를 호출하는 footer를 지정합니다. 보통 footer는 주석을 추가할 때 쓰이는데, 제가 모르는 더 나은 방법이 있을 수도 있습니다. 하지만 이 방법도 목적을 매우 잘 달성합니다.deno compile --target=<target_architecture> --output <executable_name> bundle.cjs로 번들된 파일을 단일 실행 파일로 컴파일합니다.
Node Single Executable Applications (SEA)는 실험적인 Node v23+ 기능으로, Node.js가 설치되어 있지 않은 시스템에 Node.js 애플리케이션을 배포할 수 있게 해줍니다.
이 방법의 단점은 CommonJS 파일만 지원한다는 점이며, Webpack/Parcel/Rollup/Esbuild 같은 번들러를 사용해 생성된 Gleam JavaScript 파일들을 단일 파일로 번들해야 합니다.
gleam build --target=javascript로 Gleam 프로젝트를 빌드합니다.esbuild build/dev/javascript/<project_name>/<project_name>.mjs --platform=node --minify-whitespace --minify-syntax --bundle --outfile=bundle.cjs --format=cjs --footer:js=\"main();\"
node로 지정합니다. 제 프로젝트는 내부적으로 Node.js API를 사용하고 있기 때문입니다(Gleam simplifile package).bundle.cjs로, 포맷을 cjs(CommonJS)로 지정합니다. 이는 Node SEA에 필요합니다.main 메서드를 호출하는 footer를 지정합니다. 보통 footer는 주석을 추가할 때 쓰이는데, 제가 모르는 더 나은 방법이 있을 수도 있습니다. 하지만 이 방법도 목적을 매우 잘 달성합니다.다음 단계들은 Deno보다 훨씬 더 복잡하며, here에서 확인할 수 있습니다. 제 프로젝트의 경우에는 다음과 같습니다 -
sea-config.json을 만들고 내용을 채웁니다. 여기서 중요한 필드는 main과 output입니다.node --experimental-sea-config sea-config.json로, 복사한 Node.js 바이너리에 주입할 blob을 생성합니다.cp $(command -v node) executable_name로 Node 실행 파일을 복사본으로 만듭니다.npx postject executable_name NODE_SEA_BLOB <output>.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2로 blob을 복사한 Node.js 바이너리에 주입합니다../executable_name로 바이너리를 실행합니다.위 단계를 따르면 제 경우에는 segfault가 발생했고, 이유를 잘 모르겠습니다. SEA를 더 자세히 공부한 뒤 이 섹션을 업데이트하겠습니다. 다만 Deno가 Node SEA에 비해 훨씬 단순하다는 점은 분명합니다.
컴파일 플래그를 사용한 Bun build는 여러 JS 파일을 번들한 다음, 단일 실행 파일로 컴파일하는 Bun 기능입니다. Deno compile과 비슷하지만 별도의 번들러가 필요 없습니다.
import된 모든 파일과 패키지가 Bun 런타임 사본과 함께 실행 파일에 번들됩니다. 모든 내장 Bun 및 Node.js API가 지원됩니다.
gleam build --target=javascript로 Gleam 프로젝트를 빌드합니다.bun build --compile --outfile=bundle build/dev/javascript/<project_name>/<project_name>.mjs --footer="main();"이게 끝입니다. Bun은 믿을 수 없을 만큼 편리하고, Deno나 Node 또는 지금까지 언급한 어떤 방법보다도 터무니없이 빠릅니다.
Nexe는 Node.js 애플리케이션을 단일 실행 파일로 컴파일해주는 커맨드라인 유틸리티입니다. Burrito와 마찬가지로 Nexe도 제 프로젝트에서 동작하게 만들 만큼 충분히 가지고 놀아보지는 못했지만, Gleam에서 Burrito를 동작시키는 것보다 훨씬 더 직관적일 것 같습니다.
제가 사용해 본 모든 도구/라이브러리 중에서는, Bun이 믿을 수 없을 정도로 빠르면서도 사용하기도 매우 쉬웠습니다. Bun과 Deno의 유일한 문제는 자체 런타임을 번들하기 때문에 실행 파일이 크다는 점이며, 보통 100MB를 넘습니다. 저는 실행 파일을 프로덕션에서 사용하지 않기 때문에 개인적으로는 신경 쓰지 않지만, 염두에 두어야 할 사항입니다.