Contents

백엔드 - typescript-express 환경에서 jest 및 node 환경에 따라 dotenv 설정 분리

문제상황

TDD 방법론을 따라 프로젝트를 진행하려는데 Jest를 이용하여 test 코드를 작성하던 중,
한가지 문제를 마주한다.
 

test를 위한 DB는 그 조건에 따라 생성되고 삭제되어지는 특성상,
실제 개발환경에서의 DB와 test DB를 분리하기 위해서 local DB는 2가지로 나누어 세팅을 해야한다.
 

그런데 test를 실행할때마다 typeORM 세팅의 연결값을 매번 변경해줘야하는 문제가 상당히 번거로웠다.
 

고민하던 와중, cross-env라는 npm package를 알게되었고 이를 이용하여 상황에 따라 dotenv 설정값을 유동적으로 활용할 수 있도록 세팅을 구상해보았다.
 

요약.

  1. JEST의 test파일 실행시 기존 DB를 사용함으로 인한 DB내부 데이터 오염문제
    • test용 DB 분리로 해결
  2. test용 DB 분리 이후, npm run 실행 환경(dev, test, production)에 따른 DB 설정문제
    • 개발간 서버를 돌릴때, test코드를 돌릴때마다 dev.DB와 test.DB를 따로이 설정해줘야 하는 번거로움
  3. main.ts파일에서 cross-env 패키지를 사용하더라도 JEST- test 코드는 해당 파일을 사용하지 않기에 어떻게 환경설정을 분리시킬 수 있는가에 따른 고민  

Project Tree

우선 현재 상황에서의 project folder Tree는 다음과 같다.
 


// project Tree

├── .env
├── jest.config.js
├── package.json
├── src
│   ├── app.ts
│   ├── controllers
│   │   └── users.controller.ts
│   ├── entities
│   │   └── users.entity.ts
│   ├── main.ts
│   ├── middleware
│   │   └── jwt.strategy.ts
│   ├── models
│   │   ├── index.ts
│   │   ├── repositories.ts
│   │   └── users.dao.ts
│   ├── routes
│   │   ├── index.route.ts
│   │   └── users.route.ts
│   ├── services
│   │   └── users.service.ts
│   ├── tests
│   │   ├── setup-tests.ts
│   │   └── users.test.ts
│   ├── types
│   │   └── global.d.ts
│   └── utils
│       └── util.ts
└── tsconfig.json

 

설정

고민하며 생각했던 해결방안은 다음과 같다.

  1. 일반적인 개발환경과 test를 위한 환경을 구분할 수 있는 특정값을 조건으로 할당
  2. 위에서 할당한 조건에 따라 node는 각기 다른 dotenv 파일을 참조하여 실행할 수 있도록 작성  

우선 1번을 구현하기 위해 cross-env 패키지를 이용하여 조건을 할당한다.
 

cross-env package 설치

 

$ npm i -D dotenv cross-env

 

Tip

dev [^2]

  • 개발용을 위해 response에 따라 색상이 입혀진 축약된 로그를 출력.
  • status값이 빨간색이면 서버 에러코드, 노란색이면 클라이언트 에러 코드, 청록색은 리다이렉션 코드, 그외 코드는 컬러가 없다. https://i.imgur.com/ou2hvTT.png

combined

  • 배포환경에서 사용
  • 불특정 다수가 접속하기 때문에 IP를 로그에 남겨줌

 

cli에서 명령어 입력시 정확한 입력에 주의한다.
 

Warning
crossenv 사건 [^1]
노드와 npm 생태계를 떠들썩하게 만든 사건이 있었다. 
이름하여 ‘crossenv 사건’ 이다. 
사람들이 cross-env를 설치할 때 실수로 cross-env 대신 crossenv를 설치해서 발생했는데, crossenv는 사용자의 .env 파일에 들어 있는 키들을 해커에게 전송하는 악성 패키지였던 것이었다.
다행히 문제를 발견한 즉시 패키지가 차단되어 피해가 크게 확산되지는 않았지만, 유명한 패키지를 설치하는 과정에 혼동을 야기해 해킹하려는 시도가 있었다는 점에서 충격적인 사건이었다. 
따라서 패키지를 설치할 때는 항상 주의를 기울여야 한다.

 

node setting

package.json 파일에서 명령어를 이용해 앞서 설치하였던 cross-env 패키지에서 사용할 NODE_ENV라는 이름으로 설정값을 세팅한다. (이름은 다른걸로 해도 무방하다.)
 

// package.json

"scripts": {  
  "test": "cross-env NODE_ENV=test jest --runInBand --detectOpenHandles --forceExit",    
  "start": "cross-env NODE_ENV=production node dist/main.js",  
  "dev": "cross-env NODE_ENV=develop concurrently \"npx tsc --watch\" \"nodemon -q dist/main.js\"",

 

npm 명령 스크립트의 서두에 cross-env NODE_ENV=<KEY> 라는 방식으로 세팅을 해두면, 해당 명령이 실행될때 NODE_ENV 의 값이 앞서 언급한 조건으로서 process.env.NODE_ENV라는 dotenv의 내부 값으로 할당된다.
 

예시>
 

$ npm test 
# NODE_ENV=test 라는 값으로 세팅되고 jest가 실행된다.

$ npm run dev
# NODE_ENV=dev 라는 값으로 세팅되고 ts파일의 컴파일과 nodemon이 실행된다.

 

이렇게 스크립트단에서의 명령어에 세팅을 해두고 이제 해당 값에 따라 dotenv가 적용될 수 있게끔,
설정파일의 분리를 진행한다.
 
 

dotenv 분리

우선 상황에 따른 dotenv 설정값을 별도로 작성한 파일을 만들어주는데, root 폴더에서 파일을 그대로 두자니 지저분한것 같아 따로이 폴더를 만들어 그 안에 정리하였다.
 

├── env
    ├── .env.dev
    ├── .env.production
    └── .env.test

 

typrORM의 dotenv 설정을 아래 예시로 들어본다.
차이점은,
TYPEORM_DATABASE명에서 test용과 개발용을 구분하였고,
TYPEORM_LOGGING의 경우 test환경에서는 필요가 없기에 false로 처리하였다.
 

그리고 배포시에 쓰는 .env.production 파일에서는
연결시마다 scheme 자동생성을 막기 위해 TYPEORM_SYNCHRONIZE=FALSE 처리를 해둔다.
 

예시>
 

// env/.env.dev

#typeorm  
TYPEORM_CONNECTION=mysql
TYPEORM_HOST=127.0.0.1  
TYPEORM_USERNAME=root
TYPEORM_PASSWORD=1234"  
TYPEORM_DATABASE=project_abc  // <- 여기
TYPEORM_PORT=3306  
TYPEORM_LOGGING=TRUE  // <- 여기
TYPEORM_SYNCHRONIZE=TRUE

 

// env/.env.test

TYPEORM_CONNECTION=mysql  
TYPEORM_HOST=127.0.0.1  
TYPEORM_USERNAME=root  
TYPEORM_PASSWORD=1234
TYPEORM_DATABASE=test_project_abc  // <- 여기
TYPEORM_PORT=3306  
TYPEORM_LOGGING=FALSE  // <- 여기
TYPEORM_SYNCHRONIZE=TRUE

 

그리고 끝으로 이제 앞에서 특정한 값을 실행시킬 수 있도록 express를 실행하는 파일(대개 main.ts)에서 아래와 같이 세팅을 한다. 1

 

// src/main.ts

import dotenv from 'dotenv';  
import path from 'path';  
if (process.env.NODE_ENV === 'production') {  
  dotenv.config({ path: path.join(__dirname, '/../env/.env.production') });  
} else if (process.env.NODE_ENV === 'develop') {  
  dotenv.config({ path: path.join(__dirname, '/../env/.env.dev') });  
  console.log('process.env.NODE_ENV is ', process.env.NODE_ENV);  
} else if (process.env.NODE_ENV === 'test') {  
  dotenv.config({ path: path.join(__dirname, '/../env/.env.test') });  
} else {  
  throw new Error('process.env.NODE_ENV IS_NOT_SET!!');  
}

 

console.log('process.env.NODE_ENV is ', process.env.NODE_ENV);
dev 설정시 따로 console.log를 찍어준 이유는 개발환경에서 dev설정이 제대로 먹히고 있는지 확인차 넣어줬다. 2

 

jest 세팅

exress를 돌릴때, 만약 app.ts와 main.ts(or index.ts)등으로 나누어 놓았다면, process.env.NODE_ENV 는 main.ts에 작성되기에 jest에서는 해당 설정이 적용되지 않는다.
때문에 test 파일 실행시 test용 dotenv가 작동될 수 있도록 따로 세팅을 해주어야 한다.(파일명 확인)
 


// 최상단 root폴더에 jest.config.js 또는 jest.config.json 파일

setupFiles: [  
  '<root>/src/tests/setup-tests.ts',  
],

 


// setup-tests.ts (파일명은 jest.config에서 설정했던 값과 동일하기만 하면 되기에 임의로 정해도 된다.)

import dotenv from 'dotenv';  
  
dotenv.config({  
  path: '<root>/env/.env.test',  
});

 

위에서 설명한 바와 같이 세팅을 하면, 이제 jest / test파일 실행시 해당 dotenv 설정이 작동한다.
 

typeORM.intialize()를 사용한다면 여기에 더해 실행하고자 하는 test파일 내부에서 한가지 더 코드가 필요하다.
 

// users.test.ts

const dataSource = new DataSource({
  type: process.env.TYPEORM_CONNECTION,
  host: process.env.TYPEORM_HOST,
  port: process.env.TYPEORM_PORT,
  username: process.env.TYPEORM_USERNAME,
  password: process.env.TYPEORM_PASSWORD,
  database: process.env.TYPEORM_DATABASE,
  entities: [__dirname + '/../**/*.entity.{js,ts}'],
  logging: process.env.TYPEORM_LOGGING,
  synchronize: process.env.TYPEORM_SYNCHRONIZE,
});

 

jest에서는 코드가 돌아갈때, main.ts파일을 거치지 않기때문에 typeormDataSource 코드가 돌기 전 이니셜라이징할 수 있도록 따로 준비가 필요하다.
위와 같이 설정시, 앞서 설정했던 jest.config에 따라 intialize() 코드가 돌게되고 이후부터는 앞서 설명했던대로 함수와 명령들이 실행되게 된다.


추가 : dotenv 분리로 morgan설정 역시 따로 분리 가능

 


//app.ts

const createApp = () => {  
  const app: Express = express();  
  app.use(cors(corsOptions));  
  
  if (process.env.NODE_ENV === 'develop') {  
    app.use(morgan('dev'));           // <- 이렇게
  } else {  
    app.use(morgan('combined'));      // <- 이렇게
  }  
  
  app.use(express.json());  
  app.use(express.urlencoded({ extended: false }));  
  app.use(router);  
  
  return app;  
};

 

express에서 log를 남겨주는 morgan의 옵션중 devcombined 가 있는데 이를 상황에 맞게 나눌 수 있다.
 

Tip

dev [^2]

  • 개발용을 위해 response에 따라 색상이 입혀진 축약된 로그를 출력.
  • status값이 빨간색이면 서버 에러코드, 노란색이면 클라이언트 에러 코드, 청록색은 리다이렉션 코드, 그외 코드는 컬러가 없다. https://i.imgur.com/ou2hvTT.png

combined

  • 배포환경에서 사용
  • 불특정 다수가 접속하기 때문에 IP를 로그에 남겨줌

  1. Nest.js - Deprecated - dotenv 와 cross-env 를 활용한 환경 별 환경 변수 지정하기 ↩︎

  2. 그리고 끝까지 본문을 읽으면 알겠지만 test용 dotenv설정은 따로이 jest세팅에서 진행하기에 사실, else if (process.env.NODE_ENV === 'test') { dotenv.config({ path: path.join(__dirname, '/../env/.env.test') }); 이 부분은 필요가 없다. 하지만 추후 다른 용도에서 응용을 위한 예시로써 남겨둔다.
     
      ↩︎