Logo

dotenv로 환경 변수를 .env 파일로 관리하기

많은 Node.js 프로젝트에서 환경 변수를 좀 더 효과적으로 관리하기 위해서 dotenv라는 라이브러리를 사용하고 있습니다. 이번 포스팅에서는 환경 변수를 파일에 저장해놓고 접근할 수 있게 도와주는 dotenv 라이브러리에 대해서 알아보겟습니다.

dotenv 패키지 설치

npm 패키지 매니저를 이용하여 dotenv 라이브러리를 Node.js 프로젝트에 설치합니다.

$ npm i dotenv

.env 파일 작성

dotenv 라이브러리는 아무 설정을 하지 않으면 현재 디렉토리에 위치한 .env 파일로 부터 환경 변수를 읽어오는데요. .env 파일을 생성하고, 그 안에 필요한 환경 변수를 키=값의 포맷으로 나열해보겠습니다.

.env
DB_HOST=localhost
DB_USER=root
DB_PASS=1234

이렇게 .env 파일에 저장해놓은 환경 변수들을 dotenv 라이브러리를 이용해서 process.env에 설정할 수 있는데요.

process.env가 생소하신 분들은 관련 포스팅를 통해서 Node.js에서 환경 변수 다루는 방법을 먼저 학습하시기를 추천드립니다.

본인 프로젝트가 CommonJS 기반인지 ES 모듈 기반인지에 따라 라이브러리 사용법이 약간 상이하므로 나눠서 설명드리도록 하겠습니다.

CommonJS에서 환경 변수 불러오기 (require)

먼저 Node.js에서 전통적으로 제공해왔던 모듈 시스템인 CommonJS에서 dotenv 라이브러리를 어떻게 사용하는지 알아볼께요.

프로그램을 구동할 때 제일 먼저 실행되는 자바스크립트 파일(ex. index.js, main.js)의 최상위에 다음과 같이 dotenv 라이브러를 임포트한 후 config() 함수를 호출해주기만 하면 됩니다.

index.js
require("dotenv").config();

console.log("DB_HOST:", process.env.DB_HOST);
console.log("DB_USER:", process.env.DB_USER);
console.log("DB_PASS:", process.env.DB_PASS);

예를 들어, 위 코드를 실행하면 process.env로 부터 읽어진 확경 변수가 출력되는 것을 볼 수 있습니다.

$ node index.js
DB_HOST: localhost
DB_USER: root
DB_PASS: 1234

하지만, 같은 파일 내에서 dotenv 라이브러리의 config() 함수를 호출하기 전에 process.env를 읽으면 안 되니 주의하셔야 합니다.

index.js
console.log("DB_HOST:", process.env.DB_HOST);
console.log("DB_USER:", process.env.DB_USER);
console.log("DB_PASS:", process.env.DB_PASS);

require("dotenv").config();
$ node index.js
DB_HOST: undefined
DB_USER: undefined
DB_PASS: undefined

ES 모듈에서 환경 변수 불러오기 (import)

ES 모듈을 사용하고 있는 Node.js 환경에서는 require 대신에 import 키워드를 사용해서 dotenv 패키지를 불러오면 됩니다.

index.mjs
import dotenv from "dotenv";

dotenv.config();

console.log("DB_HOST", process.env.DB_HOST);
console.log("DB_USER:", process.env.DB_USER);
console.log("DB_PASS:", process.env.DB_PASS);
$ node index.mjs
DB_HOST: localhost
DB_USER: root
DB_PASS: 1234

Node.js에서 ES 모듈(import/export) 사용하는 방법은 관련 포스팅을 참고 바랍니다.

다른 파일에 환경 변수 저장하기

만약에 .env가 아닌 다른 경로에 있는 파일에 환경 변수를 저장해야 한다면 어떻게 해야할까요?

.env.local
DB_HOST=localhost
# DB_USER=root
DB_USER=test
# DB_PASS=1234
DB_PASS=5678

그럴 때는 config() 함수를 호출 시 path 옵션에 해당 파일 경로를 넘기면 됩니다.

index.mjs
import dotenv from 'dotenv';

dotenv.config({ path: '.env.local' });
console.log('DB_HOST:', process.env.DB_HOST);
console.log('DB_USER:', process.env.DB_USER);
console.log('DB_PASS:', process.env.DB_PASS);
$ node index.mjs
DB_HOST: localhost
DB_USER: test
DB_PASS: 5678

프로그램을 실행하면서 환경변수 불러오기

dotenv를 임포트(import)하여 dotenv.config() 함수를 코드에서 호출하기 힘든 상황이라면, 프로그램을 구동할 때, node 커맨드의 -r 또는 --require 옵션으로 dotenv/config를 넘기는 방법도 있는데요 이 방법을 사용하면 dotenv 라이브러리를 코드에 직접 임포트하지 않아도 .env 파일에 저장된 환경 변수가 process.env에 설정됩니다.

우선 index.jsindex.mjs 파일을 열고 dontenv 패키지를 불러와서 dotenv.config() 함수를 호출하는 부분을 삭제하고요.

index.mjs
console.log("DB_HOST:", process.env.DB_HOST);
console.log("DB_USER:", process.env.DB_USER);
console.log("DB_PASS:", process.env.DB_PASS);

-r 옵션으로 dotenv/config를 넘겨서 실행을 해보면 정상적으로 환경 변수가 출력되는 것을 볼 수 있습니다.

$ node -r dotenv/config index.mjs
DB_HOST: localhost
DB_USER: root
DB_PASS: 1234

만약에 .env가 아닌 다른 경로에 있는 파일에 환경 변수를 저장해놨다면 DOTENV_CONFIG_PATH 환경 변수를 사용하면 됩니다.

$ DOTENV_CONFIG_PATH=.env.local node -r dotenv/config index.mjs
DB_HOST: localhost
DB_USER: test
DB_PASS: 5678

이 방법은 어떤 프로젝트가 CommonJS 기반인지 ES 모듈 기반인지 미리 알 수 없을 때 매우 유용합니다. 왜냐하면 해당 NOde.js 런타임(runtime)이 어떤 모듈 시스템을 사용하든지 상관없이 통하는 방법이기 때문입니다.

ES 모듈에서 발생하기 쉬운 실수

ES 모듈을 사용할 때는 CommonJS를 사용할 때 보다 좀 더 주의가 필요한데요. 흔히 발생하는 문제를 재현해보겠습니다.

아래 코드를 보시면, dotenv 라이브러리를 제일 임포트하기 때문에 db.js 파일이 process.env에 접근할 때 환경 변수가 설정이 되어 있을 것 같습니다.

db.mjs
export const db_host = process.env.DB_HOST;
export const db_user = process.env.DB_USER;
export const db_pass = process.env.DB_PASS;
index2.mjs
import dotenv from "dotenv";
import { db_host, db_user, db_pass } from "./db.js";

dotenv.config();

console.log("DB_HOST:", process.env.DB_HOST);
console.log("DB_USER:", process.env.DB_USER);
console.log("DB_PASS:", process.env.DB_PASS);

console.log({ db_host, db_user, db_pass });

하지만 실제 실행을 해보면 db.js 파일이 process.env에 접근했을 시점에는 환경 변수가 설정이 되어 있지 않았던 것을 알 수 있습니다.

$ node index2.mjs
DB_HOST: localhost
DB_USER: root
DB_PASS: 1234
{ db_host: undefined, db_user: undefined, db_pass: undefined }

이러한 현상이 발생하는 이유는 dotenv.config() 함수가 db.js 파일이 임포트 된 이후에 호출되었기 때문인데요. 이 문제는 dotenv 라이브러리를 임포트하는 코드를 별도의 파일로 빼고, 그 안에서 dotenv.config() 함수를 호출하면 피할 수 있습니다.

env.mjs
import dotenv from "dotenv";

dotenv.config();
index2.mjs
import "./env.js";
import { db_host, db_user, db_pass } from "./db.js";

console.log("DB_HOST:", process.env.DB_HOST);
console.log("DB_USER:", process.env.DB_USER);
console.log("DB_PASS:", process.env.DB_PASS);

console.log({ db_host, db_user, db_pass });

이제 다시 프로그램을 실행을 해보면 환경 변수가 모든 파일에서 정상적으로 읽히는 것을 볼 수 있습니다.

$ node index2.mjs
DB_HOST: localhost
DB_USER: root
DB_PASS: 1234
{ db_host: 'localhost', db_user: 'root', db_pass: '1234' }

이처럼 프로그램을 시작된 후 가급적 dotenv.config() 함수를 빨리 호출하는 것이 안전합니다.

이미 설정되어 있는 환경 변수

운영 체제 수준에서 이미 설정되어 있는 환경 변수는 dotenv를 통해 파일에서 읽어온 환경 변수 값들로 덮어써지지 않으니 주의가 필요한데요. 예를 들어, 리눅스 계열 운영체제에서 다음과 같이 프로그램을 실행하기 전에 미리 DB_PASS 환경 변수를 설정해놓으면

$ export DB_PASS=0000
$ node index.mjs

.env 파일에 설정해놓은 1234가 무시되고 0000이 적용되는 것을 볼 수 있습니다.

$ node index.mjs
DB_HOST: localhost
DB_USER: root
DB_PASS: 0000

참고로 어느 환경 변수가 이미 설정되어 있었는지는 debug 옵션을 true로 주면 쉽게 알아낼 수 있습니다.

index.mjs

import dotenv from 'dotenv';
const result = dotenv.config({ debug: true });
console.log('DB_HOST:', process.env.DB_HOST);
console.log('DB_USER:', process.env.DB_USER);
console.log('DB_PASS:', process.env.DB_PASS);
$ node index.mjs
[dotenv@16.0.3][DEBUG] "DB_PASS" is already defined in `process.env` and was NOT overwritten
DB_HOST: localhost
DB_USER: root
DB_PASS: 0000

.env 파일에 설정해놓은 환경 변수의 값이 기 설정된 환경 변수의 값을 덮어쓰기를 원한다면 (좋은 관행은 아닙니다) overridetrue로 설정하면 됩니다.

index.mjs

import dotenv from 'dotenv';
const result = dotenv.config({ debug: true, override: true });
console.log('DB_HOST:', process.env.DB_HOST);
console.log('DB_USER:', process.env.DB_USER);
console.log('DB_PASS:', process.env.DB_PASS);
$ node index2.mjs
[dotenv@16.0.3][DEBUG] "DB_PASS" is already defined in `process.env` and was overwritten
DB_HOST: localhost
DB_USER: root
DB_PASS: 0000

보안상 주의 사항

.env 파일에는 보통 데이터베이스의 비밀번호나 서드파티(3rd-party) 서비스의 API 키와 같이 민감한 인증 정보가 들어가기 때문에 GitHub와 같은 코드 저장소(repository)에 올리면 상당히 위험할 수 있습니다. 특히 협업 프로젝트에서는 .gitignore 파일에 이용하여 개발자들이 실수로라도 코드 저장소에 올릴 수 없도록 설정해놓는 것이 바람직하겠습니다.

.gitignore
.env
.env.local

뿐만 아니라, .env.production, .env.staging, .env.qa, .env.development, .env.local, .env.test 이런 식으로 각 배포(deploy) 환경 별로 환경 변수를 다른 파일에 저장해두고 사용하는 것도 심심치 않게 볼 수 있는데요. 이렇게 하면 위와 마찬가지 이유로 보안 이슈에 취약할 뿐만 아니라, 일반적으로 코드(code)와 설정(config)을 한 곳에서 관리하는 것은 좋지 않은 소프트웨어 개발 관행으로 여겨집니다.

따라서 .env 파일은 개발자가 로컬 환경에서 환경 변수를 설정해야할 때만 제한적으로 사용하는 것이 좋으며, 그 밖에 환경에서는 운영 체제 수준에서 제대로 환경 변수를 설정해줘야겠습니다.

전체 코드

본 포스팅에서 작성한 코드는 아래에서 직접 수정하거나 브라우저 상의 가상 터미널에서 실행해볼 수 있습니다.

마치면서

이상으로 dotenv 라이브러리를 이용해서 파일에 환경 변수를 저장해놓고 불러오는 방법에 대해서 자세히 살펴보았습니다. dotenv 라이브러리에 대해서 좀 더 많은 내용이 알고 싶으시다면 GitHub 저장소를 방문해보시면 좋을 것 같습니다.