Logo

Babel로 Node.js에서 ES6 코드 실행하기

ES6(ES2105) 이상의 최신 자바스크립트 문법으로 작성된 코드가 노드JS(Node.js)에서 실행이 안 되서 애를 먹는 경우가 종종 있는데요. 이번 포스팅에서는 자바스크립트 트랜스파일러(Transpiler)인 Babel을 이용하여 이 문제를 깔끔하게 해결해보겠습니다.

개발자들이 실행 환경에 구애받지 않고 항상 최신 문법의 자바스크립트로 코딩할 수 있도록 도와주는 유용한 도구인 바벨(Babel)에 대해서는 별도 포스팅을 참고바랍니다.

Node.js에서 ES6 코드 실행 오류

먼저 간단한 예제 프로젝트를 하나를 만들겠습니다. js-babel-node라는 디렉터리에 NPM 패키지를 생성합니다.

$ mkdir js-babel-node
$ cd js-babel-node
$ npm init -y

이제 다음과 같이 매우 간단한 자바스크립트 코드를 작성해보겠습니다. Node.js 내장 모듈인 path를 이용해서 프로젝트의 dist 디렉터리의 절대 경로를 출력 후에 외부에서 불러올 수 있도록 내보내는 코드입니다.

  • index.js
import path from "path";
const outputPath = path.resolve(__dirname, "dist");
console.log(`Output path is "${outputPath}".`);
export { outputPath };

그런 다음 node 커맨드를 이용해서 콘솔에서 작성한 파일을 Node.js 런타임으로 실행시켜봅니다.

$ node index.js
/home/runner/js-babel-node/index.js:1
import path from "path"
^^^^^^

SyntaxError: Cannot use import statement outside a module
    at wrapSafe (internal/modules/cjs/loader.js:1047:16)
    at Module._compile (internal/modules/cjs/loader.js:1097:27)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1153:10)
    at Module.load (internal/modules/cjs/loader.js:977:32)
    at Function.Module._load (internal/modules/cjs/loader.js:877:14)
    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:74:12)
    at internal/main/run_main_module.js:18:47

ES6에서 자바스크립트 모듈을 불러오기 위해서 사용되는 import라는 키워드 때문에 구문 예외가 발생하였습니다.

웹 브라우저의 경우 자바스크립트 문법을 쫒아오는 속도가 중구난방이지만, Node.js의 경우 최신 버전을 사용한다면 거의 왠만한 ES6 문법은 도입이 되어 있습니다. 그럼에도 불구하고 본 예제처럼 Node.js는 아직 CommonJS 기반 모듈 시스템을 사용하기 때문에 ES6의 importexport와 같은 키워드는 디폴트로 지원되지 않고 있습니다.

아쉬운데로 파일의 확장자를 .mjs로 바꾸고 --experimental-modules 옵션을 이용하면 방법도 있긴한데 아직 실험적인(Experimental) 기능이므로 여기서는 다루지는 않겠습니다. 이 부분에 대해서는 별도 포스팅에서 자세히 다루고 있으니 참고 바라겠습니다.

Babel: JavaScript Transpiler

위에서 발생한 예외를 근본적으로 해결하려면 우리가 작성한 코드를 Node.js 런타임이 실행가능한 형태로 변환해줘야 합니다. 이를 보통 transpile이라고 하는데요. 소스 코드를 머신 코드로 바꿔주는 compile과 달리, transpile은 같은 언어를 유지한체 다른 런타임에서 해당 코드가 정상적으로 해석될 수 있도록 형태만 바꿔준다는 차이가 있습니다. 실제 현장에서는 이 두 용어를 혼용해서 사용하고 있는 것 같습니다.

여기서는 JavaScript Transpiler 중에서 가장 널리 사용되는 Babel을 사용해서 위 코드를 transpile하여 Node.js 런타임이 예외없이 실행할 수 있도록 해보겠습니다.

제일 먼저 Babel의 기반이 되는 핵심 패키지를 설치합니다.

$ npm i -D @babel/core

Babel CLI

먼저 개발 의존성으로 바벨 커맨드 라인 도구를 설치하겠습니다.

$ npm i -D @babel/cli

설치를 마치면 터미널에서 babel 커맨드를 사용할 수 있게 됩니다. babel 커맨드를 사용하면 자바스크립트 코드를 transpile할 수 있습니다.

먼저 babel 커맨드로 우리가 작성한 자바스크립트 파일을 transpile 해보겠습니다.

$ npx babel index.js
import path from "path";
const outputPath = path.resolve(__dirname, "dist");
console.log(`Output path is "${outputPath}".`);
export { outputPath };

어라… 이상하네요? 각 라인의 끝에 ;가 추가된 것 빼고는 소스 코드의 형태가 처음에 작성한 그대로 입니다. 그 이유는 아직 어떻게 변환할지에 대해서 바벨 설정을 해주지 않았기 때문입니다.

Babel Preset 설정

바벨 설정은 여러 플러그인을 스스로 조합하거나 미리 준비된 프레셋을 사용할 수가 있습니다. 여기서는 env라는 가장 범용적으로 사용되는 프리셋을 사용하도록 하겠습니다. env 프리셋은 ES2015 이상의 최신 자바스크립트 문법으로 작성된 코드를 해석할 수 있습니다.

먼저 개발 의존성으로 프리셋을 설치합니다.

$ npm i -D @babel/preset-env

그리고 프리셋 옵션을 줘서 다시 한 번 바벨로 우리가 작성한 자바스크립트 파일을 변환해보겠습니다.

$ npx babel --presets @babel/env index.js
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.outputPath = void 0;

var _path = _interopRequireDefault(require("path"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

var outputPath = _path["default"].resolve(__dirname, "dist");

exports.outputPath = outputPath;
console.log("Output path is \"".concat(outputPath, "\"."));

이번에는 다행히 원하는 형태로 소스 코드가 transpile 되었습니다. import를 사용하던 부분은 Node.js에서 지원하는 require를 사용하도록 대체되었으며, export를 사용하던 부분 역시 exports 변수를 사용하도록 변경되었습니다. 그 밖에도 문자열 출력 부분이 interpolation 구문("${outputPath}")에서 concatenation("' + outputPath + '")으로 대체된 것도 확인할 수 있습니다.

Node.js로 ES6 코드 실행 성공

마지막으로 바벨에 의해서 transpiple된 코드를 노드 런타임으로 실행해보겠습니다. 다음과 같이 pipeline (|) 심볼을 이용해서 바벨 실행 결과를 노드 커맨드의 입력으로 전달해주면 됩니다.

$ npx babel --presets @babel/env index.js | node
Output path is "/home/runner/js-babel-node/dist".

Babel Node

코드의 transpile과 실행을 한 번에 할 수 있다면 얼마나 편리할까요? 특히, 윈도우 같이 pipeline 심볼을 사용할 수 없는 경우에는 더욱 절실할 것입니다. 이를 위해서 Babel에서는 babel-node 커맨드도 제공하고 있는데요. babel-node 커맨드는 @babel/node 패키지를 설치하면 사용할 수 있습니다.

$ npm i -D @babel/node

이제 babel 커맨드 대신에, babel-node 커맨드를 사용해서 코드를 transpile 후 곧바로 실행할 수 있습니다. 🎉

$ npx babel-node --presets @babel/env index.js
Output path is "/home/runner/js-babel-node/dist".

[TIP] 바벨 설정 파일

바벨 커맨드를 실행할 때 마다 매번 프리셋 옵션을 붙이기가 번거롭다면 바벨 설정 파일인 .babelrc를 프로젝트 최상위 디렉터리에 작성하시면 됩니다.

  • .babelrc
{
  "presets": ["@babel/env"],
}

다른 방법으로 .babelrc 파일 대신에 다음과 같이 package.json 파일에 바벨 설정을 추가할 수도 있습니다.

  • package.json
{
(... 생략 ...)
  "devDependencies": {
    "@babel/cli": "^7.10.4",
    "@babel/core": "^7.10.4",
    "@babel/node": "^7.10.4",
    "@babel/preset-env": "^7.10.4"
  },
+  "babel": {
+    "presets": ["@babel/env"]
+  },
(... 생략 ...)
}

위 두가지 방법 중 원하시는 방법으로 설정 후에는 프리셋 옵션을 주지 않고도 바벨 커맨드를 실행할 수 있습니다. 🤗

$ npx babel-node index.js
Output path is "/home/runner/js-babel-node/dist".

아무래도 프로젝트 규모가 커지고 바벨 설정이 복잡해지는 상황에서는 설정 파일을 이용하는 편이 훨씬 이점이 많을 것입니다.

[TIP] NPM 스크립트로 등록

바벨 커맨드를 자주 사용하는 상황이라면 NPM 스크립트로 등록해두고 사용하시는 게 더 간편할 수 있습니다.

  • package.json
{
(... 생략 ...)
  "scripts": {
    "start": "babel-node index.js"
  },
(... 생략 ...)
}
$ npm start

> js-babel-node@1.0.0 start /home/runner/js-babel-node
> babel-node index.js

Output path is "/home/runner/js-babel-node/dist".

전체 코드

마치면서

이상으로 Babel를 이용하여 Node.js 런타임에서 ES6 코드를 실행하는 방법에 대해서 알아보았습니다.