
Next.js 환경변수 설정하는 법 — .env 우선순위와 NEXT_PUBLIC 차이 총정리
process.env가 자꾸 undefined로 찍히는 이유부터 .env·.env.local 로딩 우선순위, NEXT_PUBLIC_ 접두사로 브라우저에 노출되는 값과 서버 전용 값의 차이, 빌드 시점 치환까지 — Next.js 환경변수 설정에서 헷갈리는 지점만 짚었다.
.env.local에 API_KEY를 분명히 적었다. 서버에서는 잘 나오는데, 브라우저 콘솔에서 process.env.API_KEY를 찍으면 undefined가 뜬다. Next.js를 처음 만지는 사람 거의 전부가 이 지점에서 한 번은 멈춘다.
버그가 아니다. 설계다. Next.js는 환경변수를 누구에게, 언제 보여줄지를 꽤 엄격하게 나눠 놨다. 그 경계를 모르고 쓰면 undefined가 나거나, 더 나쁘게는 시크릿이 브라우저로 새어 나간다. 규칙은 사실 세 가지뿐이고, 한 번 손에 익으면 다시 헷갈릴 일이 없다.

준비: .env 파일은 다섯 종류, 핵심은 우선순위
Next.js는 프로젝트 루트에 있는 .env* 파일을 자동으로 읽는다. dotenv를 따로 깔 필요 없다. 종류는 다섯 가지인데, 각자 역할이 다르다.
| 파일 | 언제 로드되나 | 깃 커밋 |
|---|---|---|
.env | 항상 (모든 환경 공통 기본값) | 한다 |
.env.local | 항상, 단 테스트 환경 제외 | 안 한다 |
.env.development | next dev (개발) | 한다 |
.env.production | next build / next start (운영) | 한다 |
.env.test | 테스트 (NODE_ENV=test) | 한다 |
여기에 .env.development.local 처럼 .local을 붙인 변형까지 있다. 같은 이름의 변수가 여러 파일에 흩어져 있으면 우선순위로 덮어쓴다. 위에 있을수록 세다.
process.env (실제 시스템 환경변수, 가장 셈)
└ .env.$(NODE_ENV).local 예: .env.development.local
└ .env.local (테스트 환경에선 안 읽음)
└ .env.$(NODE_ENV) 예: .env.production
└ .env (가장 약함)
이 표를 통째로 외울 필요는 없다. 실무에서 쓰는 규칙은 딱 하나다. 팀이 공유하는 기본값은 .env에, 내 PC에만 있는 값·비밀값은 .env.local에. .env.local이 항상 위라서, 공용 설정을 건드리지 않고 내 로컬만 조용히 바꿔치울 수 있다. 그래서 .env.local은 깃에 올리지 않는 게 기본이다 — 사람마다 다른 값이니까.
1단계 — 서버에서만 쓸 값은 접두사 없이
DB 비밀번호, 결제 API 시크릿처럼 브라우저가 절대 몰라야 하는 값은 접두사 없이 그냥 적는다.
# .env.local
DATABASE_URL=postgres://user:pw@localhost:5432/app
STRIPE_SECRET_KEY=sk_live_xxxxxxxx
이 값들은 서버 쪽 코드에서만 읽힌다. 서버 컴포넌트, Route Handler(app/api/.../route.ts), getServerSideProps 같은 곳이다.
// app/api/pay/route.ts (서버에서 실행)
export async function POST() {
const key = process.env.STRIPE_SECRET_KEY; // 정상 동작
// ...결제 처리
}
같은 코드를 "use client" 컴포넌트에 넣으면 undefined가 된다. 접두사 없는 변수는 클라이언트 번들에 아예 포함되지 않기 때문이다. 불편해 보이지만 이건 안전장치다. 깜빡하고 시크릿을 프런트에 뿌리는 사고를 Next.js가 기본값으로 막아 준다.
2단계 — 브라우저에서 써야 하면 NEXT_PUBLIC_
공개돼도 되는 값만 이름 앞에 NEXT_PUBLIC_을 붙인다. 공개 사이트 주소, 공개 분석 키 같은 것들이다.
# .env
NEXT_PUBLIC_SITE_URL=https://example.com
NEXT_PUBLIC_GA_ID=G-XXXXXXX
// 클라이언트 컴포넌트에서도 읽힌다
export default function Footer() {
return <a href={process.env.NEXT_PUBLIC_SITE_URL}>home</a>;
}
접두사 하나가 "이 값은 브라우저에 박혀서 전 세계에 공개된다"는 선언이 된다. 뒤집어 말하면, 이름에 NEXT_PUBLIC_을 붙이는 순간 그 값은 더 이상 비밀이 아니다. 누구나 브라우저 개발자 도구로 꺼내 볼 수 있다. 그러니 시크릿엔 절대 붙이면 안 된다.
| 접두사 없음 | NEXT_PUBLIC_ | |
|---|---|---|
| 읽히는 곳 | 서버 전용 | 서버 + 브라우저 |
| 번들 포함 | 안 됨 | 됨 (문자열로 박힘) |
| 쓰는 값 | 시크릿·DB·토큰 | 공개 URL·공개 키 |
3단계 — "빌드 시점에 박힌다"를 이해하면 끝난다
여기서 사람들이 제일 많이 넘어진다. NEXT_PUBLIC_ 값은 앱이 돌 때 환경변수를 읽는 게 아니다. next build 하는 그 순간 코드 안으로 문자열이 그대로 치환돼 들어간다.

// 내가 작성한 코드
fetch(process.env.NEXT_PUBLIC_API_URL + "/users")
// 빌드가 끝난 번들 (값이 그대로 박힘)
fetch("https://api.example.com" + "/users")
여기서 두 가지가 따라온다. 첫째, NEXT_PUBLIC_ 값을 바꾸면 반드시 다시 빌드해야 반영된다. 컨테이너 환경변수만 갈아끼우고 재배포하면 옛날 값이 그대로 번들에 박혀 있다. 둘째, 접두사 없는 서버 변수는 런타임에 읽으니 재빌드 없이 환경변수만 바꿔도 된다. 둘의 동작이 정반대다.
그래서 Docker로 Next.js를 올릴 땐 NEXT_PUBLIC_*을 빌드 인자(build arg) 로 넘겨 이미지에 구워 넣는 게 정석이다. 런타임에 -e로만 주면 서버 변수는 바뀌어도 브라우저용 값은 꿈쩍도 안 한다.
흔한 오류 — undefined 체크리스트
process.env.X가 undefined일 때, 위에서부터 짚으면 거의 다 걸린다.
- 브라우저에서 읽는데
NEXT_PUBLIC_을 안 붙였다. 압도적으로 가장 흔하다. 서버에선 되고 클라이언트에서만 undefined면 십중팔구 이거다. .env를 고치고 개발 서버를 재시작 안 했다..env는 서버가 켜질 때 한 번만 읽는다. 저장만으로는 안 먹으니next dev를 껐다 켠다.- 변수명을 문자열로 조립해서 읽었다.
process.env["NEXT_PUBLIC_" + name]같은 동적 접근은 치환되지 않는다. 빌드가 값을 박으려면process.env.NEXT_PUBLIC_FOO처럼 통째로 적어야 한다. - 값에 따옴표·공백이 섞였다.
KEY="abc"는 따옴표까지 값에 들어갈 수 있다. 그냥KEY=abc로 쓴다. .env.local이.gitignore에 있어서 배포 서버엔 안 올라갔다. 이건 정상이다. 배포 환경에는 그 환경의 변수를 콘솔이나 시크릿 매니저로 따로 넣는다.
팁 하나. .env.example에 키 이름만 빈 값으로 적어 커밋해 두면, 새로 합류한 팀원이 무슨 변수를 채워야 하는지 한눈에 본다. 값은 비우고 이름만 남기는 게 포인트다.
개인적으로 제일 많이 당한 건 세 번째가 아니라 Docker 재배포 함정이었다. 로컬에선 멀쩡한데 운영에서만 API 주소가 옛날 걸 가리켜서 한참을 헤맸는데, 원인은 NEXT_PUBLIC_API_URL을 런타임 환경변수로만 주입하고 재빌드를 안 한 거였다. 국내 팀 중 상당수가 Vercel 대신 자체 서버나 쿠버네티스에 Next.js를 올리는데, 이 경우 빌드와 배포 단계가 분리돼 있어 이 함정에 잘 빠진다. "환경변수를 바꿨는데 왜 안 바뀌지"의 열에 아홉은 NEXT_PUBLIC_과 빌드 시점 치환을 헷갈린 경우다. 반대로, 서버 전용 변수를 바꿀 때마다 꼬박꼬박 재빌드를 돌리고 있다면 그건 그것대로 시간 낭비다. 그건 런타임에 바뀌니까. 결국 "이 값이 번들에 박히는 값인가, 서버가 그때그때 읽는 값인가" — 이 경계 하나만 머릿속에 그려 두면 Next.js 환경변수는 거의 다 끝난 셈이다.
한 장 요약
- Next.js는 루트의
.env*를 자동 로드. 우선순위는.local> 환경별(.development/.production) >.env. - 비밀값은 접두사 없이 → 서버 전용. 공개값만
NEXT_PUBLIC_→ 브라우저에 노출. NEXT_PUBLIC_은 빌드 시점에 문자열로 박힌다 → 값 바꾸면 재빌드. Docker는 build arg로 넘긴다.undefined면 ① 접두사 빠졌나 ② 서버 재시작 했나 ③ 동적 접근 아닌가 순으로 점검..env.local은 커밋 금지. 대신.env.example로 키 이름만 공유한다.
참고
댓글 0
댓글을 불러오는 중…





