Next.js 앱을 배포하다 보면 API 키가 브라우저 번들에 노출되는 실수를 흔히 저지른다. 서버에서만 써야 할 시크릿이 클라이언트 자바스크립트에 포함되면, 누구나 개발자 도구로 꺼낼 수 있다. Next.js는 NEXT_PUBLIC_ 접두사 하나로 이 경계를 명확히 구분한다.
빌드 타임
├── NEXT_PUBLIC_* → 번들에 인라인 삽입 → 브라우저에서 읽힘
└── 그 외 변수 → 서버 런타임에만 존재 → 브라우저 접근 불가
런타임 (서버)
├── process.env.DB_URL (서버 컴포넌트 / Route Handler)
├── process.env.NEXT_PUBLIC_URL (서버 + 클라이언트 양쪽)
└── (브라우저) process.env.DB_URL → undefined
빌드 시 주입(build-time injection): Next.js가 next build 시점에 NEXT_PUBLIC_ 변수를 리터럴 값으로 번들에 삽입하는 방식. 런타임에 변경해도 반영되지 않는다.
Next.js는 아래 순서로 .env 파일을 병합한다. 위쪽이 우선권을 가진다.
| 파일 | 용도 | 커밋 여부 |
|---|---|---|
.env.local | 로컬 오버라이드 | 커밋 안 함 (.gitignore) |
.env.development | 개발 환경 기본값 | 커밋 |
.env.production | 프로덕션 기본값 | 커밋 |
.env | 모든 환경 공통 | 커밋 |
.env.local은 항상 가장 높은 우선순위를 가지며, 시크릿 키는 여기에만 보관한다.
// app/api/data/route.ts — 서버에서만 실행됨
export async function GET() {
const db = await connect(process.env.DATABASE_URL!) // 서버 전용
const rows = await db.query('SELECT * FROM posts')
return Response.json(rows)
}
서버 컴포넌트와 Route Handler에서는 NEXT_PUBLIC_ 없이도 모든 환경 변수에 접근할 수 있다.
// components/Analytics.tsx — 클라이언트 번들에 포함됨
'use client'
export function Analytics() {
// NEXT_PUBLIC_ 접두사가 없으면 undefined
const id = process.env.NEXT_PUBLIC_GA_ID
return <script data-ga={id} />
}
# .env.local
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
DATABASE_URL=postgresql://user:secret@host/db
빌드 후 번들을 확인하면 G-XXXXXXXXXX는 문자열 리터럴로 삽입되어 있고, DATABASE_URL은 흔적조차 없다.
절대 NEXT_PUBLIC_로 시작하면 안 되는 변수들:
DATABASE_URL — DB 접속 정보JWT_SECRET — 토큰 서명 키STRIPE_SECRET_KEY — 결제 API 시크릿SMTP_PASSWORD — 이메일 서버 비밀번호실수로 노출됐다면, 해당 서비스에서 키를 즉시 갱신해야 한다. 번들에 박힌 값은 재배포 전까지 누구나 읽을 수 있다.
환경 변수는 기본적으로 string | undefined다. 런타임에 누락된 변수를 잡으려면 앱 시작 시점에 검증 로직을 둔다.
// lib/env.ts
function requireEnv(key: string): string {
const value = process.env[key]
if (!value) throw new Error(`환경 변수 누락: ${key}`)
return value
}
export const env = {
databaseUrl: requireEnv('DATABASE_URL'),
gaId: process.env.NEXT_PUBLIC_GA_ID ?? '',
}
서버 전용 변수(server-only variable): NEXT_PUBLIC_ 접두사가 없는 변수. Node.js 프로세스 메모리에만 존재하며, 클라이언트 번들에 포함되지 않는다.
규칙은 단순하다. 브라우저에서 읽어야 하면 NEXT_PUBLIC_을 붙이고, 그렇지 않으면 붙이지 않는다.