.env 파일 문법과 환경변수를 다룰 때 알아야 할 것들
.env 파일은 KEY=value 를 한 줄에 하나씩 적는 단순한 형식입니다. 그런데 이 '단순함'을 규정하는 공식 표준이 없어서, 따옴표 처리나 줄바꿈 같은 세부 동작이 라이브러리마다 다릅니다. 이 도구는 붙여넣은 .env 내용을 키-값으로 분해해 보여 주므로, 파서가 실제로 무엇을 읽어 들이는지 확인할 수 있습니다.
기본 문법
키는 관례적으로 대문자와 밑줄만 씁니다. 셸에서 환경변수로 export될 수 있어야 하기 때문이며, 하이픈이 들어간 키는 셸에서 다룰 수 없습니다. 등호 주변에 공백을 넣는 것도 피하세요. KEY = value 로 쓰면 파서에 따라 키가 "KEY "가 되거나 값이 " value"가 됩니다.
# 주석은 # 으로 시작 NODE_ENV=production PORT=3000 # 값에 공백이 있으면 따옴표 APP_NAME="My Application" # 빈 값도 유효 OPTIONAL_FLAG= # URL처럼 특수문자가 많은 값 DATABASE_URL=postgres://user:pass@localhost:5432/mydb # 여러 줄 값 (지원하는 파서에서만) PRIVATE_KEY="-----BEGIN PRIVATE KEY----- MIIEvQIBADANBg... -----END PRIVATE KEY-----"
사용 방법
- .env 파일 내용을 붙여넣습니다.
- 파싱된 키-값 목록을 확인합니다. 의도한 값이 정확히 들어갔는지, 따옴표가 값에 포함되지 않았는지 봅니다.
- 값이 잘려 있다면 아래의 따옴표·특수문자 항목을 확인하세요.
- 결과를 코드나 배포 설정 화면에 옮겨 씁니다.
따옴표가 필요한 경우와 그 부작용
실무에서 가장 자주 겪는 사고가 비밀번호에 들어간 $ 와 # 입니다. 일부 파서는 $VAR 를 다른 환경변수 참조로 확장하고, # 이후를 주석으로 버립니다. 그 결과 비밀번호가 조용히 잘려서 인증 실패로 나타나는데, 로그에는 비밀번호를 찍지 않으니 원인 파악이 오래 걸립니다. 특수문자가 포함된 값은 작은따옴표로 감싸는 것이 가장 안전합니다.
| 작성 방식 | 파싱 결과 | 비고 |
|---|---|---|
| KEY=hello | hello | 가장 단순한 형태 |
| KEY="hello world" | hello world | 따옴표는 제거됨 |
| KEY=hello world | hello world 또는 hello | 파서에 따라 다름 — 따옴표를 쓰세요 |
| KEY="line1\nline2" | 줄바꿈이 포함된 값 | 큰따옴표에서만 \n 해석 |
| KEY='line1\nline2' | 문자 그대로 line1\nline2 | 작은따옴표는 이스케이프 미해석 |
| KEY=value # 주석 | value 또는 value # 주석 | 인라인 주석 지원 여부가 다름 |
| KEY=pa$$word | pa 또는 pa$$word | $ 는 변수 확장으로 해석될 수 있음 |
환경변수는 문자열뿐이다
운영체제 수준에서 환경변수의 값은 항상 문자열입니다. 여기서 흔한 버그가 나옵니다. DEBUG=false 로 설정해도 process.env.DEBUG 는 문자열 "false" 이고, JavaScript에서 비어 있지 않은 문자열은 참이므로 if (process.env.DEBUG) 는 통과합니다. 숫자도 마찬가지로 PORT=3000 은 "3000" 이어서 산술 연산에서 문자열 연결이 일어납니다.
그래서 환경변수는 읽는 즉시 한 곳에서 검증하고 변환하는 패턴이 권장됩니다. zod나 envalid 같은 라이브러리로 스키마를 정의하면, 필수 값이 빠졌을 때 애플리케이션이 시작 시점에 명확한 오류로 죽습니다. 런타임 중간에 undefined로 이상하게 동작하는 것보다 훨씬 낫습니다.
import { z } from "zod";
const env = z.object({
NODE_ENV: z.enum(["development", "production", "test"]),
PORT: z.coerce.number().default(3000),
DEBUG: z.stringbool().default(false),
DATABASE_URL: z.string().url(),
}).parse(process.env);
// 이후 코드에서는 env.PORT 가 실제 number
export default env;우선순위와 파일 분리
대부분의 라이브러리는 이미 설정된 실제 환경변수를 .env 파일 값으로 덮어쓰지 않습니다. 즉 셸에서 export한 값이나 컨테이너에 주입된 값이 이깁니다. 이는 의도된 설계입니다. 배포 환경에서는 플랫폼이 주입한 값이 파일보다 우선해야 하기 때문입니다.
여러 파일을 쓰는 관례도 있습니다. .env 는 공통 기본값, .env.local 은 개인 로컬 설정(커밋 제외), .env.production 은 환경별 값입니다. Next.js나 Vite 같은 도구는 이 순서를 정해 두고 로드하므로, 값이 예상과 다르면 어느 파일이 최종적으로 이겼는지 확인해야 합니다.
보안에서 반드시 지킬 것
- .env 를 .gitignore 에 넣습니다. 대신 .env.example 에 키 이름만 적고 값은 비워 두어 팀이 필요한 항목을 알 수 있게 합니다.
- 이미 커밋했다면 히스토리에서 지우는 것으로 끝내지 마세요. 노출된 자격증명은 즉시 교체해야 합니다. 저장소가 공개였다면 이미 자동 스캐너에 수집되었다고 가정하는 편이 안전합니다.
- 클라이언트에 노출되는 변수를 구분하세요. Next.js의 NEXT_PUBLIC_, Vite의 VITE_ 접두사가 붙은 값은 브라우저 번들에 그대로 포함됩니다. 이 접두사를 붙인 변수에 비밀값을 넣으면 그 즉시 공개됩니다.
- 운영 환경의 비밀값은 파일보다 시크릿 매니저(AWS Secrets Manager, GCP Secret Manager, Vault)에 두는 것이 안전합니다. 접근 감사와 교체가 가능해집니다.
- CI 로그에 환경변수를 덤프하지 마세요. env 나 printenv 출력이 로그에 남으면 로그 접근 권한이 있는 모두에게 노출됩니다.
자주 묻는 질문
- 붙여넣은 내용이 서버로 전송되나요?
- 전송되지 않습니다. 파싱은 브라우저에서 이루어집니다. 그렇더라도 운영 환경의 실제 자격증명이 담긴 .env 를 외부 사이트에 붙여넣는 것은 권장하지 않습니다. 문법을 확인하려는 목적이라면 값을 더미로 바꿔서 확인하세요.
- 값에 줄바꿈을 넣으려면 어떻게 하나요?
- 파서가 지원하는 경우 큰따옴표로 감싸고 실제 줄바꿈을 넣거나 \n 이스케이프를 씁니다. 다만 지원 여부가 라이브러리마다 달라, RSA 개인키처럼 여러 줄인 값은 실무에서 Base64로 한 번 인코딩해 한 줄로 저장하고 코드에서 디코딩하는 방식을 자주 씁니다. 플랫폼 UI에 직접 입력하는 경우에는 대개 여러 줄이 그대로 지원됩니다.
- 값에 # 이 들어가면 잘립니다.
- 인라인 주석으로 해석된 것입니다. 값 전체를 따옴표로 감싸면 해결됩니다. 비밀번호에 #, $, 공백, 따옴표 같은 문자가 있으면 작은따옴표로 감싸는 것이 가장 예측 가능합니다.
- 왜 .env 값이 적용되지 않나요?
- 확인 순서는 이렇습니다. 파일이 프로세스의 작업 디렉터리에 있는지(하위 디렉터리에서 실행하면 못 찾습니다), 로드 코드가 다른 import보다 먼저 실행되는지, 이미 셸에 같은 이름의 환경변수가 설정되어 있어 이기고 있는지, 그리고 파일 이름이 정확한지(.env.txt 로 저장된 경우가 흔합니다).
- 키에 하이픈을 쓸 수 있나요?
- 일부 파서는 읽어 들이지만 쓰지 마세요. 하이픈이 있는 이름은 셸에서 환경변수로 export할 수 없어 도구 간 이동이 불가능해집니다. 밑줄을 쓰는 것이 관례이자 실용적인 선택입니다.
- 빈 값과 정의되지 않은 값은 다른가요?
- 다릅니다. KEY= 는 빈 문자열이 설정된 상태이고, 아예 없으면 undefined 입니다. JavaScript에서 process.env.KEY ?? "default" 는 빈 문자열을 유지하지만 process.env.KEY || "default" 는 빈 문자열을 기본값으로 대체합니다. 이 차이가 의도치 않은 동작을 만들 수 있으니 어느 쪽이 필요한지 정해서 쓰세요.
💡 참고: 값이 이상하게 들어갔다고 느껴지면 이 도구로 파싱 결과를 먼저 확인하세요. 따옴표가 값에 포함되어 있거나 특수문자에서 잘려 있는 경우가 대부분입니다.