본문 바로가기
개발 팁

.env & 환경변수 관리 실전 — 팀 프로젝트부터 실수 대처법까지

by 루까(Luka) 2026. 3. 17.
반응형

왜 이 글을 쓰나

팀 프로젝트를 시작하면 꼭 한 번씩 벌어지는 일이 있다. 누군가 .env 파일을 GitHub에 올리거나, API 키가 없어서 로컬에서 실행이 안 되거나, .env.example이 오래되어 실제 변수랑 맞지 않거나. 규칙을 만들어두지 않으면 반복된다.

이 글에서는 기본 설정부터 팀 공유 방법, 실수로 커밋했을 때 복구까지 실무에서 쓰이는 방식을 정리한다.


.env 기본

Node.js 기준. dotenv 패키지를 설치하면 .env 파일의 변수를 process.env로 읽을 수 있다.

npm install dotenv
# .env
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
JWT_SECRET=my-super-secret-key
PORT=3000
NODE_ENV=development
// app.js (엔트리 포인트 최상단에서 한 번만)
require('dotenv').config();

const dbUrl = process.env.DATABASE_URL;
const port = process.env.PORT || 3000;

dotenv.config()는 반드시 다른 모듈 임포트 전에 호출해야 한다. 순서가 틀리면 변수가 undefined로 읽힌다.


.gitignore 설정

.env는 반드시 .gitignore에 등록해야 한다. 프로젝트 생성 시점에 잡아두지 않으면 나중에 실수가 생긴다.

# .gitignore

# 환경변수 — 절대 커밋하지 않는다
.env
.env.local
.env.*.local
.env.production
.env.staging

# 예외: 예시 파일은 커밋한다
# !.env.example

.env.example은 커밋 대상이다. 실제 값 없이 키 이름과 설명만 담는다.

# .env.example
DATABASE_URL=           # PostgreSQL 연결 문자열
API_KEY=                # 서비스 API 키 (발급처: https://example.com/api)
JWT_SECRET=             # 최소 32자 이상의 랜덤 문자열
PORT=3000               # 기본값 3000
NODE_ENV=development    # development | staging | production

신규 팀원이 프로젝트를 받으면 .env.example을 복사해서 값만 채운다.

cp .env.example .env

팀에서 환경변수를 공유하는 법

.env를 Slack이나 이메일로 공유하는 팀이 아직도 많다. 문제가 있다.

방법 문제점
Slack DM으로 공유 로그에 남음, 퇴사자도 조회 가능
이메일 첨부 암호화 없음, 전달 실수 위험
Git에 커밋 최악. 영구 기록
.env.example + 직접 전달 중간 정도. 체계가 없음

실무에서 쓰는 방법들을 정리한다.

방법 1. 1Password / Bitwarden (팀 비밀번호 관리자)

가장 권장하는 방식이다. Secure Note나 항목으로 환경변수를 저장하고, 팀원을 초대해서 공유한다. 접근 권한 관리도 된다.

방법 2. Vault (HashiCorp)

엔터프라이즈 환경에서 많이 쓴다. CLI로 시크릿을 읽어서 환경변수로 주입할 수 있다.

# Vault에서 값 읽기
export API_KEY=$(vault kv get -field=api_key secret/myapp)

방법 3. GitHub Actions / CI 환경변수

CI/CD에서는 .env를 쓰지 않는다. GitHub Actions의 Secrets에 등록하고 워크플로우에서 참조한다.

# .github/workflows/deploy.yml
- name: Deploy
  env:
    DATABASE_URL: ${{ secrets.DATABASE_URL }}
    API_KEY: ${{ secrets.API_KEY }}
  run: npm run deploy

방법 4. dotenv-vault

dotenv를 만든 팀이 만든 공식 SaaS다. .env.vault 파일을 커밋하고, 복호화 키(DOTENV_KEY)만 공유한다.

npx dotenv-vault push   # 환경변수 업로드
npx dotenv-vault pull   # 최신 환경변수 다운로드

소규모 팀에 잘 맞는다.


환경별 분리

로컬/스테이징/프로덕션 환경에서 다른 값을 써야 할 때.

.env                    # 로컬 기본값
.env.staging            # 스테이징 전용
.env.production         # 프로덕션 전용 (서버에만 존재)

dotenv에서 환경별 파일을 로드하려면:

require('dotenv').config({
  path: `.env.${process.env.NODE_ENV}`
});

또는 dotenv-flow 패키지를 쓰면 파일 우선순위를 자동으로 처리해준다.

.env → .env.local → .env.{NODE_ENV} → .env.{NODE_ENV}.local

실수로 커밋했을 때 대처법

.env를 커밋했다는 걸 뒤늦게 발견했을 때. 단계별로 처리한다.

1단계. 즉시 시크릿 무효화

Git에서 파일을 지워도 이미 기록에 남아 있다. 먼저 노출된 API 키, DB 비밀번호, JWT 시크릿을 즉시 재발급/변경해야 한다. 이게 가장 중요하다.

2단계. 현재 브랜치에서 파일 제거

# 파일을 Git 추적에서 제거 (실제 파일은 유지)
git rm --cached .env

# .gitignore에 추가
echo ".env" >> .gitignore

git add .gitignore
git commit -m "remove .env from tracking"

3단계. Git 이력에서 완전 삭제

파일이 과거 커밋에 남아 있다면 이력 자체를 수정해야 한다.

# git-filter-repo 사용 (권장)
pip install git-filter-repo
git filter-repo --path .env --invert-paths

# 또는 BFG Repo-Cleaner 사용
java -jar bfg.jar --delete-files .env
git reflog expire --expire=now --all
git gc --prune=now --aggressive

4단계. 강제 푸시

git push origin --force --all
git push origin --force --tags

팀 저장소라면 팀원들에게 알려서 로컬 브랜치를 다시 받게 해야 한다.

GitHub의 경우, 이력에서 지워도 캐시가 남을 수 있다. GitHub 고객지원에 캐시 삭제 요청을 별도로 보내야 완전히 사라진다.


검증 스크립트

팀 프로젝트에서 필수 환경변수가 빠지면 서버가 뜨다가 이상하게 동작하거나 NPE가 난다. 시작 시점에 검증 로직을 추가하면 문제를 초기에 잡는다.

// config/env.js
const required = [
  'DATABASE_URL',
  'API_KEY',
  'JWT_SECRET',
];

const missing = required.filter((key) => !process.env[key]);

if (missing.length > 0) {
  console.error('[ENV ERROR] 다음 환경변수가 없습니다:');
  missing.forEach((key) => console.error(`  - ${key}`));
  process.exit(1);
}

module.exports = {
  databaseUrl: process.env.DATABASE_URL,
  apiKey: process.env.API_KEY,
  jwtSecret: process.env.JWT_SECRET,
  port: parseInt(process.env.PORT, 10) || 3000,
};

앱 시작 시 바로 죽으면서 어떤 변수가 빠졌는지 알려준다.


방법별 비교

방법 보안 편의성 팀 규모 비용
.env 직접 전달 (Slack/이메일) 낮음 높음 소규모 무료
1Password / Bitwarden 높음 중간 소~중규모 유료
dotenv-vault 중간 높음 소~중규모 무료~유료
HashiCorp Vault 매우 높음 낮음 중~대규모 무료(셀프호스팅)
GitHub Actions Secrets 높음 높음 CI/CD 전용 무료

삽질 기록

dotenv가 읽히지 않는다

가장 흔한 원인 세 가지다.

1. config() 호출 순서

// 잘못됨
const db = require('./db');  // 여기서 이미 process.env를 읽음
require('dotenv').config();

// 올바름
require('dotenv').config();  // 먼저 호출
const db = require('./db');

2. 파일 경로 문제

dotenv.config()는 현재 작업 디렉토리 기준으로 .env를 찾는다. 서브 디렉토리에서 실행하면 못 찾는다.

require('dotenv').config({ path: require('path').resolve(__dirname, '../.env') });

3. 이미 존재하는 환경변수는 덮어쓰지 않는다

CI 환경에서 이미 DATABASE_URL이 설정되어 있으면 .env의 값이 무시된다. 의도된 동작이지만 헷갈린다. 강제로 덮어쓰려면:

require('dotenv').config({ override: true });

.env.example이 오래됐다

새 변수를 추가하면서 .env.example을 업데이트하지 않는 경우다. 신규 팀원이 MISSING KEY 에러를 만난다. 이걸 방지하려면 PR 체크리스트에 "새 환경변수 추가 시 .env.example 업데이트" 항목을 추가하는 게 현실적이다.

프로덕션에 development 값이 들어간다

NODE_ENV를 설정 안 한 채로 배포하면 로컬 .envdevelopment 설정이 그대로 올라가는 경우가 있다. 배포 스크립트에서 NODE_ENV=production을 명시적으로 설정하는 습관이 중요하다.


마무리

환경변수 관리를 귀찮다고 미루면 결국 사고가 난다. 프로젝트 초반에 세 가지만 해두면 된다.

  1. .env.gitignore에 등록한다
  2. .env.example을 만들고 팀 내 규칙을 정한다
  3. 시작 시점에 필수 변수 검증 로직을 추가한다

나머지는 팀 규모와 보안 요구사항에 맞게 점진적으로 개선하면 된다. 처음부터 Vault를 도입할 필요는 없다.


기술 스택: Node.js, dotenv, dotenv-vault, HashiCorp Vault, GitHub Actions

반응형