Node.js 기반에서 환경변수 사용하기 (dotenv, cross-env)

public_danuel·2019년 12월 7일
14
post-thumbnail

환경변수가 뭐죠?

엄밀한 정의는 아니지만, 환경변수는 특정 process를 위한 key-value 형태의 변수라고 할 수 있습니다.
예를 들면, Java 기반으로 개발을 하기 위해서 JDK를 설치할 때에 환경변수 경로를 설정해줘야 하는 경우가 있겠네요.

Node.js 기반이라면 process.env[key] 형태로 사용할 수 있습니다.
process.env.NODE_ENV 를 떠올리셨다면 맞아요. 이미 익숙하게 사용해오던, Node.js 기반에서의 대표적인 환경변수 중 하나이죠.

크로스플랫폼 환경변수 설정

프로젝트 참여자 각각이 MacOS, Windows, Linux 등 다양한 OS를 사용하고 있다면 여러 난감한 상황이 발생합니다.
OS 마다 환경변수를 설정하는 방법이 다르다는 것이 그 중 하나입니다.

이에 대해 Node.js 커뮤니티에서 내놓은 몇몇 대책을 소개하겠습니다.

dotenv 라이브러리

# npm
npm i -s dotenv

# Yarn
yarn add dotenv

1번째로는 dotenv 라이브러리를 설치해서 사용하는 것입니다.
이 라이브러리는 미리 작성해놓은 .env 파일을 환경변수에 대신 설정해주는 기능을 가지고 있습니다.

import dotenv from 'dotenv'

dotenv.config()

위처럼 작성하면 현재 디렉토리의 .env 파일을 자동으로 인식해서 환경변수를 설정해줍니다.

import path from 'path'
import dotenv from 'dotenv'

dotenv.config({ path: path.join(__dirname, 'path/to/.env') })

위처럼 작성하면 원하는 .env 파일의 위치를 직접 지정할 수 있습니다.
이렇게 하면 production, develop, local, test 등 상황에 맞게 환경변수를 작성한 후 적절하게 사용할 수 있겠죠.

이 정도만 해도 충분히 괜찮다고 생각할 수 있지만, 이 방법은 아쉬운 점이 있습니다.

+ src
  - index.develop.js
  - index.local.js
  - index.production.js
  - index.test.js
  - ...
- .env.develop
- .env.local
- .env.production
- .env.test
- package.json
- ...

원하는 상황의 개수 만큼 진입점 파일을 따로 작성을 해줘야 한다는 것입니다.
물론, 이미 각 상황에 따라 진입점이 다르다면 괜찮지만 번거로운 것은 여전합니다.

cross-env 라이브러리

# npm
npm i -s cross-env

# Yarn
yarn add cross-env

2번째로는 cross-env 라이브러리를 설치해서 사용하는 것입니다.
이 라이브러리는 프로그램을 CLI 환경에서 실행시킬 때에 환경변수를 설정하는 기능을 가지고 있습니다.

cross-env NODE_ENV=production node src/index.js

위처럼 사용할 수 있습니다. MacOS/Linux와 Windows는 환경변수를 설정하는 방법이 다른데, OS의 이러한 상황과 관계 없이 설정하기 위해 사용합니다.

import path from 'path'
import dotenv from 'dotenv'

if (process.env.NODE_ENV === 'production') {
  dotenv.config({ path: path.join(__dirname, 'path/to/.env.production') })
} else if (process.env.NODE_ENV === 'develop') {
  dotenv.config({ path: path.join(__dirname, 'path/to/.env.develop') })
} else {
  throw new Error('process.env.NODE_ENV를 설정하지 않았습니다!')
}

이 라이브러리를 이용하면 1개의 진입점으로도 상황에 맞게 환경변수를 사용할 수 있겠죠.

환경변수 관리

환경변수는 Git/SVN과 같은 버전컨트롤시스템에서 관리하지 않는 것이 좋습니다 .
GitHub 등 Git 호스팅 서비스를 이용한다면 Private Repository가 아닌 경우 그대로 노출이 되는 등 보안적 으로 여러 문제가 있기 때문 입니다.

그러나, 버전컨트롤시스템의 제어를 벗어난 다른 방법으로 환경변수를 관리하다 보면 꼭 설정해줘야 하는 환경변수를 누락하기도 합니다.
이는 잠재적으로, 의도하지 않은 동작을 일으키기도 하고, 트러블 슈팅 난이도를 올리는 원인으로 작용하기도 합니다.

import Sequelize from 'sequelize'

const sequelize = new Sequelize({
  database: process.env.DATABASE,
  port: process.env.PORT,
  username: process.env.USERNAME,
  password: process.env.PASSWORD,
  dialect: process.env.DIALECT,
  dialectOptions: {
    connectTimeout: Number(process.env.CONNECT_TIMEOUT)
  }
})

또한, 환경변수는 value가 항상 string 이라는 단점 아닌 단점이 있습니다.
Node.js 기반 프로젝트를 둘러보면 위 예시처럼 필요한 곳에서 직접 타입을 변환해 사용하기도 하죠.

환경변수는 어디에서이든 접근할 수 있다는 특성 때문에, 잘 관리하는 것이 어렵기도 합니다.

다음 섹션부터는 제가 참여중인 프로젝트에서 환경변수를 관리하는 방법을 소개하고자 합니다.
(본인만의 방법 혹은 이 보다 더 좋은 방법이 있다면 소개 부탁드립니다!)

설정모음

환경변수는 어찌 보면 설정모음이라는 성격을 가지고 있습니다.
이에 착안해 configs라는 변수에 담고, 환경변수가 필요한 곳에서는 이 configs 변수를 불러와 사용을 합니다.

// configs 변수
export const configs = {
  database: process.env.DATABASE || 'localhost',
  port: process.env.PORT || 3306,
  username: process.env.USERNAME || 'root',
  password: process.env.PASSWORD || 'admin',
  dialect: process.env.DIALECT || 'mysql',
  connectTimeout: Number(process.env.CONNECT_TIMEOUT || 1000)
}

// 환경변수가 필요한 곳
import Sequelize from 'sequelize'
import { configs } from 'path/to/configs'

const sequelize = new Sequelize({
  database: configs.database,
  port: configs.port,
  username: configs.username,
  password: configs.password,
  dialect: configs.dialect,
  dialectOptions: {
    connectTimeout: configs.connectTimeout
  }
})

환경변수가 필요한 곳에서는 configs 변수를 불러와서 사용한다는 규칙을 지키면 프로젝트에서 쓰고 있는 환경변수가 얼마나 있는지 파악하기도 편리합니다.
사용하지 않는 환경변수를 추적하기에도 용이하구요.

사용하는 환경변수의 개수 만큼 작성해야 한다는 단점이 있지만, 환경변수를 추가할 때에 1번 작성한 이후에는 수정할 일이 거의 없으므로 이 정도는 감수할 만합니다.
또한, 다음 섹션에서 소개할 cast 헬퍼 함수를 이용하면 환경변수 누락까지도 한 곳에서 방지할 수 있습니다.

cast 헬퍼 함수

const number = (value: string) => {
  const result = Number(value)
  if (!Number.isNaN(result)) {
    return result
  }
}

const string = (value: string) => value

const typeConverter = { number, string }

const cast = (key, type, defaultValue) => {
  const value = process.env[key]
  if (value !== undefined) {
    const result = typeConverter[type](value)
    if (result !== undefined) {
      return result
    }
    throw new Error(`process.env.${key}에 적절한 값을 설정하지 않았습니다`)
  }
  if (defaultValue !== undefined) {
    return defaultValue
  }
  throw new Error(`process.env.${key}에 할당할 값이 없습니다`)
}

key와 type을 지정하면 환경변수에서 해당 key의 value를 읽어와 지정한 타입으로 변환을 해주는 기능을 하는 헬퍼 함수입니다.

key가 없거나 적절한 변환 결과가 없으면(즉, 값이 undefined이면) 해당 key에 대한 적절한 에러를 던집니다. (만약 defaultValue가 있다면 그것을 변환 결과로 return 합니다.)

export const configs = {
  database: cast('DATABASE', 'string', 'localhost'),
  port: cast('PORT', 'number', 3306),
  username: cast('USERNAME', 'string', 'root'),
  password: cast('PASSOWRD', 'string', 'admin'),
  dialect: cast('DIALECT', 'string', 'mysql'),
  connectTimeout: cast('CONNECT_TIMEOUT', 'number', 1000)
}

앞서 작성한 cast 헬퍼 함수를 이용해서 다시 작성하면 이렇게 할 수 있습니다.

위 예시에서 알 수 있듯이, 프로그램을 실행시킬 때에 모든 환경변수를 적절히 설정한 경우에만 에러 없이 동작하므로, 에러가 없다면 누락한 환경변수가 없다는 것을 보장할 수 있습니다.

const boolean = (value: string) => {
  switch (value) {
    case 'true': {
      return true
    }
    case 'false': {
      return false
    }
  }
}

// ...

const typeConverter = { boolean, number, string }

// ...

const configs = {
  isLogging: cast('IS_LOGGING', 'boolean', false),
  // ...
}

typeConverter는 유연해서, 위 예시처럼 원하는 변환 함수를 추가하면 원하는 만큼 유연하게 타입을 변환할 수 있습니다.

간단하게 소개를 하기 위해 기능을 많이 축소시켰지만, cast 헬퍼 함수를 조금 더 확장하면 number 타입인 경우에는 min, max 체크를 하는 등의 기능도 추가할 수 있습니다.

하니팁

  • Node.js 기반이라면 프로덕션 환경에서는 process.env.NODE_ENVproduction 으로 설정해주세요. 어떤 라이브러리는 프로덕션 모드에 대한 최적화코드를 따로 작성해놔서 퍼포먼스 향상이 있을 수 있거든요.
  • React 등 Babel/webpack 기반 개발환경이라면 process.env.NODE_ENV 는 직접 사용하는 것을 추천해요. production용 빌드라면 process.env.NODE_ENV 조건이 걸린 코드를 날려주기도 하거든요.
profile
다뉴하는 코딩

3개의 댓글

comment-user-thumbnail
2019년 12월 7일

좋아요와 댓글 감사합니다.
오탈자, 질문 등은 언제든지 댓글로 달아주세요!

답글 달기
comment-user-thumbnail
2020년 2월 20일

글 잘 읽었습니다!
궁금한게 있는데, 가장 마지막에 적어주신 바벨/웹팩 기반의 경우에 직접 사용한다는 말씀이 이해가 잘 안돼서요! 어떤 의미인지 궁금합니다 ㅎㅎ

1개의 답글