pkg는 zeit에서 만든 Node.js 바이너리 컴파일러이다. 보통 Node.js로 애플리케이션을 만들면 실행 머신에 애플리케이션에 맞는 Node.js가 설치되어 있어야 하고 npm installl
로 관련 모듈을 설치하고 npm start
나 다른 실행방식으로 애플리케이션을 실행하거나 사용해야 한다. 사용자가 다 Node.js 개발자라면 큰 문제가 아니지만 타 언어 개발자나 일반 사용자를 생각하면 편한 환경은 아니라고 할 수 있다. pkg
가 Node.js 컴파일러라는 의미는 Node.js까지 내장해서 하나의 실행 파일로 다른 의존성 없이 실행할 수 있게 만들어 준다.
이런 방식이 편하다고 느끼기 시작한 건 Go 언어로 만들어진 프로그램을 사용하면서부터다. 나 같은 경우는 HashiCorp에서 만든 Vault, Consul, Terraform 등을 사용하면 이런 방식이 편하다고 생각하게 되었다. 내 맥북에 Go 언어 환경이 없어도 바이너리 파일 하나만 다운로드 받아서 실행하면 되었고 하나의 파일로 서버와 클라이언트를 모두 사용할 수 있어서 꽤 편했다. 보통 다른 언어의 프로그램을 다운받아서 사용하려면 한두 번씩은 의존성 문제나 버전이 일치하지 않는 문제로 고생했던 터라 더 편하게 느껴진 것 같다.
pkg를 위한 환경 구성
pkg를 사용하기 위한 간단한 예제를 만들어 보자. 애플리케이션의 종류는 여기서 크게 중요하지 않으므로 Express.js의 제너레이터를 사용해서 기본 Express 앱을 만들어 보자.
$ npx express .
warning: the default view engine will not be jade in future releases
warning: use `--view=jade' or `--help' for additional options
create : .
create : ./package.json
create : ./app.js
create : ./public
create : ./routes
create : ./routes/index.js
create : ./routes/users.js
create : ./views
create : ./views/index.jade
create : ./views/layout.jade
create : ./views/error.jade
create : ./bin
create : ./bin/www
create : ./public/stylesheets
create : ./public/stylesheets/style.css
install dependencies:
$ cd . && npm install
run the app:
$ DEBUG=pkg-test:* npm start
create : ./public/javascripts
create : ./public/images
바이너리를 만들기 전에 의존성을 설치되어 있어야 하므로 npm install
로 의존성을 설치한다.
pkg
를 사용하기 위해 npm install --save-dev pkg
로 설치한다. pkg
는 빌드용 모듈이므로 devDependencies
로 추가했다.
{
...
"bin": {
"app": "./bin/www"
},
"scripts": {
"build": "pkg ."
},
...
}
pkg
명령어를 사용하려고 package.json
의 스크립트로 추가했다. 이렇게 하면 npm run build
로 pkg .
명령어를 실행할 수 있다.(요즘은 npm으로 전역 모듈은 거의 설치하지 않는 편이다) 그리고 pkg
가 바이너리를 만들기 위한 엔트리 포인트를 지정해야 하는데 bin
프로퍼티를 이용해서 지정한다. 여기서 express
애플리케이션의 실행하는 파일, 즉 시작 파일이 ./bin/www
이므로 이 파일을 지정했다. 이 시작 파일을 기준으로 pkg
가 require()
를 추적하고 필요한 파일을 찾아서 컴파일한다.
추가로 Express 예제 같은 경우 템플릿으로 Jade를 사용하고 있다. 이 뷰 엔진 같은 경우는 require()
로 불러오는 것이 아니라 아래와 같이 사용하게 된다.
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'jade');
그래서 pkg
가 이 views
디렉터리를 자동으로 바이너리 파일에 포함하지 않는다. 이런 경우 바이너리 파일에 넣어줄 파일(다른 수정 없이)을 직접 지정해 주어야 하는데 package.json
에 다음과 같이 지정할 수 있다.
{
...
"pkg": {
"assets": "views/**/*"
}
}
pkg로 바이너리 생성하기
$ npm run build
> pkg-test@0.0.0 build /Users/outsider/example
> pkg .
> pkg@4.3.1
> Targets not specified. Assuming:
node8-linux-x64, node8-macos-x64, node8-win-x64
기본 타겟 플랫폼인 Linux와 macOS, Windows를 위한 바이너리가 생성되었다.
.
├── [ 40M] example-linux
├── [ 40M] example-macos
└── [ 28M] example-win.exe
위처럼 생성된다. 기본 express 예제는 포함된 의존성이 많지 않음에도 Node.js가 바이너리 안에 포함했기 때문에 용량이 적지는 않다. 여기서 example
이라는 이름은 package.json
의 name
에서 온 것이다.
실제 이 파일이 Linux에서 다른 의존성 없이도 실행될 수 있는지 확인해 보자. Docker로 Ubuntu에서 실행해 보려고 Dockerfile
을 다음과 같이 만들었다.
FROM ubuntu:18.04
ADD example-linux ./
ENV DEBUG example:server
CMD ./example-linux
이 도커 이미지를 만들어서 실행하면 다음과 같이 잘 실행되는 것을 알 수 있다.
$ docker build -t pkg-example .
$ docker run --rm -it -p 3000:3000 pkg-example
example:server Listening on port 3000 +0ms
당연히 여기서는 ubuntu:18.04
도커 이미지를 사용했으므로 Node.js나 다른 환경은 전혀 설정되어 있지 않으므로 pkg
로 컴파일한 바이너리 하나만으로 Express 애플리케이션을 실행할 수 있는 걸 알 수 있다.(사실 Windows에서는 직접 테스트해보지 않았다.)
pkg 옵션
위에서는 pkg .
로 컴파일할 위치만 지정했지만 몇 가지 옵션을 지정할 수 있다.
$ pkg . --out-path dist
위처럼 --out-path
를 지정하면 dist
디렉토리 하에 바이너리가 생성된다.
$ pkg . --targets node8-linux-x64,node8-macos-x64,node8-win-x64
타겟 플랫폼을 지정하고 싶다면 --targets
옵션을 사용하면 된다. 타겟 플랫폼의 지정 방식은 node8-linux-x64
처럼 node${n}
로 Node.js 버전, freebsd
, linux
, macos
, win
의 플랫폼 이름, x64
, x86
, armv6
, armv7
로 아키텍처를 지정해 주면 된다.
직접 서버에서 실행하는 경우에는 이렇게 컴파일하는 것에 큰 이점이 없지만 배포하는 프로그램의 경우에는 사용자가 쉽게 사용할 수 있어서 충분히 장점이 된다고 생각한다. Node.js가 포함되어 용량이 큰 것은 좀 문제이지만 상황에 따라서는 그 부분을 무시할 수 있는 이점을 준다고 본다.
감사합니다~