본문 바로가기

javascript/typescript

[nodejs] nodemon + ts-node을 es module 환경에서 사용하기


 nodemon은 프로젝트 내에서 지정된 파일을 감시하여 변경이 발생하는 경우 앱을 재시작하는 기능을 가진 패키지로, 개발 환경에서 코드가 변경될 때마다 수동으로 앱을 재시작해야 하는 부담을 덜어준다. 

 ts-node는 내부적으로 타입스크립트 컴파일러를 이용하여 생성된 자바스크립트 파일을 실행해주는 패키지이다. 이러한 동작은 어디까지나 "내부적"으로 처리되기 때문에 개발 환경에서는 자바스크립트 파일이 생성되지 않는다. tsconfig.json 파일 내에 outDir을 따로 지정해두지 않으면 타입스크립트 파일이 있는 위치에 컴파일되어 코드가 상당히 난잡하게 섞이는데, ts-node을 이용하면 이런 지저분한 모습을 감출 수 있다. 따라서 배포를 고려하지 않는다면 ts-node는 나쁘지 않다.


CommonJS 환경에 대한 설정

 ts-node는 기본적으로  CommonJS 환경을 기준으로 동작한다. 이 기본적인 환경은 다음과 같은 설정을 요구한다.

nodemon.json

{
    "watch": ["src"], // 감시할 폴더 또는 파일의 이름들을 지정
    "ext": "ts,json", // 감시할 확장자를 지정
    "ignore": ["src/**/*.spec.ts"], // 무시할 확장자를 지정
    "exec": "ts-node ./src/index.ts" // 변경이 발생했을 때 수행할 명령을 지정
}

 nodemon은 기본 값으로 js 확장자 파일을 감시하고, nodejs를 이용하여 실행한다. 이때 타입스크립트 환경에서 동작하기 위해서는 (1) 감시할 확장자를 'ts' 로 변경하고, (2) 변경이 발생했을 때 실행할 명령을 node에서 ts-node로 변경해야 한다. 위 설정은 언급한 사항에 대응된다.

tsconfig.json(컴파일)

{
  "compilerOptions": {
    "module": "CommonJS"
    // 이외의 설정들...
  }
}

 module 옵션은 타입스크립트 코드를 어떤 환경으로 컴파일할지 지정한다. 기본적으로 CommonJS로 지정되어 있으며, 이 상태에서 컴파일하면 require/exports 기반으로 동작한다. 만약 __dirname이나 __filename 같은 옵션을 죽어도 사용해야겠다면 es module 기반에서는 지원되지 않으므로 CommonJS 환경을 이용해야 한다.

package.json(실행)

{
  "type": "commonjs"
  // 이외 다른 옵션들...
}

 package.json 파일은 현재 앱을 node로 실행할 때 지정할 설정을 저장한다. type 옵션은 패키지를 어떻게 해석할지 지정하는 옵션으로, commonjs와 module 중 선택할 수 있다. 누락하면 기본적으로 commonjs로 해석하기 때문에 ts-node을 CommonJS 환경에서 사용한다면 따로 건드릴 필요 없는 옵션이다.

 ts-node가 자바스크립트 파일을 생성하지는 않지만, 결국 현재 앱을 실행하기 위해서는 package.json 파일의 설정을 따라야 한다. 만약 사용자가 타입스크립트 파일들이 CommonJS로 컴파일되더라도 문제가 없다면 상관이 없지만, 자바스크립트 파일도 es module 형식으로 컴파일 및 실행되기를 원한다면 말이 달라진다. ts-node를 사용하든 node를 사용하든 현재 프로젝트에서는 동일한 package.json을 참조하여 노드 환경을 실행해야 하는데, 개발 환경은 CommonJS, 배포 환경은 es module으로 컴파일되기를 바라는 상황이기 때문이다. 이를 해결하기 위한 방법은 크게 2가지 존재한다.

  1. 개발 환경과 배포 환경에서 사용하는 설정 파일들을 다르게 한다. (완전히 비추천)
  2. 개발 환경을 es module로 맞춘다. 즉, ts-node을 es module 모드로 실행한다.

 1번은 너무 귀찮은 작업이기도 하고, 미묘한 설정 차이 때문에 개발 환경과 배포 환경에 차이가 발생하여 두배로 고생할 수도 있으므로 추천하지 않는다. 따라서 es module로 컴파일 및 실행하고 싶은 경우 ts-node도 es module 환경에서 실행해야 한다. 어떤 글을 보면 이게 안된다는 말이 있던데, ts-node는 컴파일에는 tsconfig.json, 실행에는 package.json의 설정을 참조해서 실행되는 방식이므로 일부 설정만 변경하면 es module 기반으로 동작하게 만들 수 있다.

es Module 환경에 대한 설정

 ts-node도 어차피 로컬의 tsconfig.json 및 package.json 파일을 참고해서 코드를 컴파일하고 실행하기 때문에 언급한 설정 파일들이 es Module에 대해 컴파일  및 동작하도록 코드를 변경해줄 필요가 있다. nodemon에는 환경 관련 정보가 없으므로 그대로 두고, 언급한 2개의 설정 파일의 일부 설정을 수정한다.

tsconfig.json (컴파일)

{
  "compilerOptions": {
    "module": "ESNext",
    "esModuleInterop": true
    // 이외의 설정들...
  },
  "ts-node": {
    "esm": true
  }
}

 es module을 기준으로 컴파일해야 하므로 module을 ES~으로 지정한다. 구체적인 버전이 필요하지 않다면 ESNext로 최신 버전을 기준으로 컴파일하도록 만드는게 마음이 편한데, 이유 없이 아래 버전을 이용하면 상위 버전에는 있는 기능을 괜히 polyfill 할 수 있기 때문이다. 실행 환경이 노드의 특정 버전으로 고정된다든지, 특정 버전 이하만 인식하는 경우에만 구체적인 버전을 고려하자.

 새로 등장한 esModuleInterop 옵션은 es Module에 대해 CommonJs을 기준으로 작성된 코드들이 호환되도록 돕는 기능이다. 과거에 작성된 모듈은 CommonJS 또는 그 이하의 방식으로 작성되어 있어 es module 환경과 호환되지 않을 수 있다. 이런 상황에서 둘이 호환될 수 있도록 돕는다.

 컴파일 옵션 이외에 ts-node라는 부분이 추가되었다.  여기에 명시된 "esm" 옵션은 ts-node가 es module 환경으로 인식하여 실행할지 지정한다. 따라서 이걸 true로 설정한다.

package.json (실행)

{
  "type": "module"
}

 현재 패키지의 실행 타입을 모듈로 지정한다.

 위 언급한 옵션들을 지정하면 es module 환경으로 ts-node를 실행할 수 있게 된다.

위 옵션을 기반으로 ts-node을 실행한 모습
위 옵션으로 컴파일한 index.js 파일의 내용


github 주소: https://github.com/blaxsior/default-setting/tree/nodemon-tsnode-esmodule