6 minute read

개요

환경설정 값들을 따로 모아 관리를 해야 할 필요성을 느낀다.
나의 경우 JAVA, SPRING 에서 properties를 여러 개(로컬, 개발, 스테이징, 운영) 만들어서 운영했던 것 처럼 구현이 필요하다.

환경변수 목적

  • 로컬, 개발, 운영 환경별로 매번 수정해서 배포하는 것은 쉽지 않다.
  • 데이터베이스의 비밀번호나 서드파티(3rd-party) 서비스의 API 키, DB 연결, 포트 설정 등의 민감한 인증 정보GitHub와 같은 코드 저장소(repository)에 올리면 상당히 위험할 수 있기 때문에 대상 보통 OS환경에서 환경 변수로 저장해놓고 사용하는 것이 권장된다.

노드JS 의 환경변수

# Linux 환경변수 (전체)
$ env

# Node.js 의 환경변수 (전체)
> process.env

위 리눅스OS 환경변수 처럼 노드JS 에서도 환경변수를 관리한다.
그리고 리눅스OS 환경변수와 마찬가지로 프로세스가 재시작되면 환경변수는 소멸된다.

# 노드 프로세스에서 적용한 환경변수는 재시작하면 사라진다.
$ node
> process.env.PORT
undefined 

기본 사용방법

# Node.js 환경변수 정의
$ PORT=3000 node

# Node.js 환경변수 확인
> process.env
{
  PORT: '3000',  #(적용)
  SHELL: '/bin/bash',
  JAVA_HOME: '/usr/lib/jvm/java-8-openjdk-armhf',
  (중략...)  
}

// Node.js 환경변수 사용(자바스크립트)
// const { PORT, DB_PASSWORD } = process.env;  // 2줄인 경우
var PORT = process.env.PORT;
console.log(PORT);  // 3000

process.env.PORT=2000 node  // 오버라이드
var PORT = process.env.PORT;
console.log(PORT);  // 2000

Node.js에서는 보통 process.env를 통해서 환경 변수에 접근한다.
. process는 Node.js에 기본적으로 내장된 전역 객체여서 별도로 불러올(import) 필요없이 프로그램의 어디에서든지 사용할 수 있다.

컨피그파일 확장자 유형

대표적인 컨피그 파일 유형은 아래와 같다.

  1. *.json
  2. .env
  3. *.yml (YAML files)
  4. *.ini (normally Windows-only)

컨피그파일 네이밍

보통 설정 파일 이름에서는 - 대신 .을 사용하는 것이 일반적이다.
자바던 노드던 properties-dev 또는 properties.dev와 같은 이름을 선택해야 한다면, 일반적으로는 properties.dev와 같은 형태가 더 많이 사용된다. - 대신 .을 사용하는 것이 일반적이며, .dev, .prod 와 같은 의미를 가진 확장자를 사용하여 개발 환경을 구분하는 것이 일반적이다.

JAVA에서 자주 사용되는 컨피그파일 네이밍

properties라는 단어는 일반적으로 Java의 properties 파일에서 많이 사용되는 용어였다.

# JAVA에서 자주 사용되는 컨피그파일 네이밍
touch properties-dev
touch properties.dev  #(추천)

Node.js 에서 자주 사용되는 컨피그파일 네이밍

# Node.js 에서 자주 사용되는 컨피그파일 네이밍
touch config.js
touch .env.development
touch .env.production

컨피그 구현 방법

  1. OS 환경변수로 구현
  2. dotenv 모듈로 구현(.env 파일)
  3. config 모듈로 구현(.json 파일)
  4. 기본 json 으로 구현(.json 파일)
  5. package.json 으로 구현

특히 env 방식이 자주 사용되는듯 하다.

CASE1. OS 환경변수로 구현

터미널

# OS레벨에서 환경변수 export
$ export PORT=3000

/server.js

const app = require('./app.js');
const PORT = process.env.PORT  //3000 (OS레벨의 환경변수를 Node가 인식한다.)

app.listen(PORT, () => {
    console.log('서버가 실행됩니다.. http://127.0.0.1:' + `{PORT}` + '/');
});

추가 내용

이미 알고있겠지만 리눅스OS가 리붓되면 환경변수는 사라진다. 아래와 같이 환경변수를 영구적으로 저장하자.

# 리눅스 환경변수 영구적으로 저장하기
$ vi ~/.bashrc
$ export PORT=3000
$ source ~/.bashrc # 또는 $sudo reboot

리눅스를 다룬다면 아래 내용은 기본적으로 알고있자. :)
로긴후 터미널 오픈시점 수행 파일

  • 전체계정: etc/bash.bashrc
  • 특정계정: 계정홈디렉토리/.bashrc

CASE2. dotenv 모듈로 구현(.env 파일)

.
├── .env    # .env파일을 루트에 생성한다.
├── app.js
└── config
(중략)

특징

  • .env 파일 기본 위치는 root 디렉토리 이다.
      /* 루트 디렉토리의 .env 호출 (이름이 붙지 않은 .env 파일을 호출함.) */
      require('dotenv').config();  
        
    
  • 다른 위치에 있거나 이름이 붙은 env파일(prod.env 같은) .env 파일을 참조하는 방법
      /* 이름이 다른 .env 호출(1) */
      require('dotenv').config({
          path : ".env.sample"
      });
    
      /* 이름이 다른 .env 호출(2) */
      dotenv.config({ path: `.env.${process.env.NODE_ENV}` });
    
      /* 다른 위치에 존재하는 .env 호출 */
      dotenv.config({ path: `${__dirname}/config/.env.${process.env.NODE_ENV}` });
    
    

require(‘dotenv’).config({ path : “.env.sample” });

장점

  • OS 레벨에서 관리하지 않아도 된다.
  • dotenv의 공식문서는 n개의 .env파일을 갖지 않을것 을 강력히 권고 하고있다. 즉, .env파일은 하나만 가지므로 한번만 .gitignore에 추가해주면 된다.

단점

  • 관리 파일이 딱 1개 이지만 .env파일 역시 config 파일과 마찬가지로 버전 관리 시스템에 공유되지 않도록 신경 써줘야하는 번거로움이 존재한다.
  • .env 환경설정 파일은 .gitignore에 추가하여 못보게 해야한다.

.env파일이 process.env에 로드 되기 전에 접근하게 되면 undefined 되므로 .env를 불러오는 코드는 가능한 코드의 최상단에 위치시켜주는 것을 권장한다.

# 그리고 들어가기전에..
# 이전에 설정한 OS환경변수를 깨끗히 제거! 잊지말자 :)  
$ unset PORT

/.env

# 환경변수 정의
PORT=3000
NODE_ENV=development

[npm install dotenv]

npm install dotenv

/server.js

require('dotenv').config();    // dotenv 모듈로 .env 환경변수를 참조
const app = require('./app.js');
const PORT = process.env.PORT  // OS없이 환경변수 사용이 가능해졌다!

app.listen(PORT, () => {
    console.log('서버가 실행됩니다.. http://127.0.0.1:' + `{PORT}` + '/');
});

.gitignore

node_modules
.env # 등록 (GIT 업로드 제외처리)

CASE3. config 모듈로 구현(.json 파일)

mkdir /config/conf_json
touch /config/conf_json/default.json
touch /config/conf_json/development.json
touch /config/conf_json/production.json

└── config
     ├── conf_json  # JSON 설정파일 전용 디렉토리 및 파일생성
         ├── default.json
         ├── development.json
         └── production.json

/config/conf_json/development.json

{
  "serverPort": 3000, 
  "database": {
      "user": "admin", 
      "pw": "123"
  }
}

[npm install config]

npm install config

/server.js

const config = require('config');  // config 모듈 참조
const app = require('./app.js');
const PORT = config.get('serverPort');  //config 모듈 사용

app.listen(PORT, () => {
    console.log('서버가 실행됩니다.. http://127.0.0.1:' + `{PORT}` + '/');
});

CASE4.기본 json 으로 구현(.json 파일)

└── config
     ├── conf_json  # JSON 설정파일 전용
        ├── default.json
        ├── development.json
        └── production.json

/config/conf_json/development.json

# 내용 동일

/server.js

// fs 모듈과 JSON.parse() 함수를 사용하여 설정정보를 읽는다.
const fs = require('fs'); 
const config = JSON.parse(fs.readFileSync('./config/conf_json/development.json', 'utf-8'));
const app = require('./app.js');
const PORT = config.serverPort;

app.listen(PORT, () => {
    console.log('서버가 실행됩니다.. http://127.0.0.1:' + `{PORT}` + '/');
});

CASE5. package.json 으로 구현

/package.json

"scripts": {
  "test": "echo \"Error: no test specified\" && exit 1",
  "dev": "NODE_ENV='development' PORT=3000 node server.js",
  "prod": "NODE_ENV='production' PORT=3000 node server.js"
}

/server.js

const app = require('./app.js');
const PORT = process.env.PORT || 3000;  //정말 환경변수가 잡힌다!.
process.env.NODE_ENV = process.env.NODE_ENV || 'development';  //정말 환경변수가 잡힌다!.

app.listen(PORT, () => {
    console.log('서버가 실행됩니다.. http://127.0.0.1:' + `{PORT}` + '/');
});

실행

npm run dev 
npm run prod 

NODE_ENV 작업환경 분기 로직

/server.js

// (중략)
process.env.NODE_ENV = process.env.NODE_ENV || 'development';
if (process.env.NODE_ENV == 'development') {
    module.exports = require('./dev');  //개발  
} else if (process.env.NODE_ENV == 'production') { 
    module.exports = require('./prod') ; //운영
} else {
    module.exports = require('./local') ; //로컬
}

app.listen(PORT, () => {
    console.log('서버가 실행됩니다.. http://127.0.0.1:' + `{PORT}` + '/');
});

/dev.js

// 개발 서버
module.exports = {
    mongoURI:'개인 mySqlDB'
};

/prod.js

// 운영 서버
module.exports = {
    mongoURI: process.env.MYSQL_URI
};

예시


module.exports = {
  // 데이터베이스 연결 정보
  db: {
    host: 'my-production-db-host',
    username: 'my-production-db-username',
    password: 'my-production-db-password',
    database: 'my-production-db'
  },
  // 로깅 관련 설정
  logging: {
    level: 'info',
    format: 'combined',
    file: '/var/log/my-app.log'
  },
  // 세션 관련 설정
  session: {
    secret: 'my-production-session-secret',
    maxAge: 86400000, // 1 day
    store: new RedisStore({
      host: 'my-redis-host',
      port: 6379,
      password: 'my-redis-password'
    })
  }
};

최종 설정파일 구현

mkdir ./config/conf_env

touch ./config/conf_env/.env.local
touch ./config/conf_env/.env.development
touch ./config/conf_env/.env.production

├── server.js  
├── config
│   ├── conf_env              #.env 설정파일
│   │   ├── .env.development
│   │   ├── .env.local
│   │   └── .env.production
│   └── config.js

/sever.js

// AS-IS (메인 .env 로드)
//require('dotenv').config();

const config = require('./config/config.js');
const app = require('./app.js');
const PORT = process.env.PORT || 3000;

app.listen(PORT, () => {
    console.log('/******************************************************');
    console.log('서버가 실행됩니다.. http://127.0.0.1:' + PORT + '/');
    console.log('환경설정파일:  ' + process.env.DESCRIPTION);
    console.log('******************************************************/');    
});

/config/config.js

// 1. 로드시 값이 없으면 무조건 [로컬]환경으로 설정.
process.env.NODE_ENV = process.env.NODE_ENV || 'local'; 

// 2. 환경변수에 따른 설정파일 로드 완료. (잘못된 코드 ==> 삽질만 3시간)
// const dotenv = require('dotenv').config({ path : `path: ${__dirname}/config/conf_env/.env.${process.env.NODE_ENV }` })

// 2. 환경변수에 따른 설정파일 로드 완료. (정상적인 코드)
const path = require('path');
const dotenv = require('dotenv').config({ path: path.resolve(__dirname, `./conf_env/.env.${process.env.NODE_ENV}`)});

// 3. 환경변수에 따른 설정파일 미존재시 에러처리
if (dotenv.error) {
    console.log('Couldnt find .env file');
    throw new Error("Couldn't find .env file️");
}

module.exports = dotenv;

/config/conf_env/.env.local

# 환경변수 정의
DESCRIPTION=로컬서버
DB_HOST="blang.co.kr"
DB_PORT="로컬서버 포트"
DB_USER="로컬서버 아이디"
DB_PASSWORD="로컬서버 패스워드"
DB_DATABASE="로컬서버 DB명"
DB_CONNECTIONLIMIT=10

/config/conf_env/.env.development

# 환경변수 정의
DESCRIPTION=개발서버
DB_HOST="blang.co.kr"
DB_PORT="개발서버 포트"
DB_USER="개발서버 아이디"
DB_PASSWORD="개발서버 패스워드"
DB_DATABASE="개발서버 DB명"
DB_CONNECTIONLIMIT=10

/config/conf_env/.env.production

# 환경변수 정의
DESCRIPTION=운영서버

DB_HOST="blang.co.kr"
DB_PORT="운영서버 포트"
DB_USER="운영서버 아이디"
DB_PASSWORD="운영서버 패스워드"
DB_DATABASE="운영서버 DB명"
DB_CONNECTIONLIMIT=10

실행

$ npm run dev   # process.env.NODE_ENV=development 으로 설정 후 메인 실행
$ npm run prod  # process.env.NODE_ENV=production 으로 설정 후 메인 실행
$ node server.js  # 값이 없으므로 process.env.NODE_ENV=local 환경으로 실행

NPM 으로 실행한다. 한참동안 설정파일을 못읽어서 삽질했다..
내용은 다음과 같다.

  • 잘못된 코드 분석
    path: 라는 문자열이 문자열 리터럴 안에 포함되어 있고 ${__dirname} 의 결과와 함께 문자열을 구성하고 있다. 이 경우에는 ${__dirname} 이 결과값을 포함하는 문자열 리터럴을 먼저 만들고, 그 문자열을 config() 함수의 path 옵션 값으로 전달하는 것이 좋다.

결론만 말하자면 path.resolve() 함수로 인자로 전달된 경로들을 연결하여 절대 경로를 생성한 결과로 path path 객체를 설정하고 .config() 인자로 전달해야 에러가 발생하지 않았다.

정리

  1. 최초 로드시점에 환경변수(process.env.NODE_ENV)를 셋팅하고 로드한다.
    • package.json 파일 스크립트에서 환경변수 셋팅
    • npm run 셋팅네임
  2. 메인실행 최초라인에서 환경경변수 값에 따라 환경설정 파일(.env)을 로드한다.
    • 환경변수 값 == .env.{환경변수 값} 방식으로 파일을 찾는다.

Leave a comment