내 API 키가 왜 undefined지? (Next.js 환경변수와 보안 사고)
1. "분명히 .env에 넣는데..."
Next.js로 날씨 앱을 만들던 중이었습니다.
OpenWeatherMap API 키를 발급받고, .env 파일에 소중히 모셔뒀죠.
# .env.local
WEATHER_API_KEY=abcdef123456
그리고 컴포넌트에서 자신 있게 로그를 찍어봤습니다.
// WeatherComponent.tsx
'use client';
console.log(process.env.WEATHER_API_KEY); // 결과: undefined
"어라? 오타 났나?"
서버를 껐다 켜보고, 캐시를 지워보고, 컴퓨터를 재부팅해봐도 결과는 똑같이 undefined였습니다.
근데 이상하게 console.log가 터미널(서버)에는 찍히는데, 브라우저 콘솔에만 안 찍히는 겁니다.
2. Next.js의 "보안 방화벽"
알고 보니 이건 버그가 아니라 Next.js의 강력한 보안 기능이었습니다.
브라우저는 사용자의 컴퓨터입니다. 누구나 F12(개발자 도구)를 눌러서 코드를 볼 수 있죠.
만약 process.env의 모든 변수가 브라우저로 전송된다면?
AWS_SECRET_KEY노출DB_PASSWORD노출- 해커: "감사합니다. 잘 쓰겠습니다."
그래서 Next.js는 기본적으로 모든 환경 변수를 서버(Server Side)에만 둡니다. 브라우저(Client Side)로 보내는 건 명시적으로 허용된 것들뿐입니다. 이것이 바로 Next.js의 Server-Client Boundary(서버-클라이언트 경계) 정책입니다.
3. 해결책 1 - "야, 이건 공개해도 돼" (NEXT_PUBLIC_)
브라우저에서 환경 변수를 쓰고 싶다면, 변수명 앞에 NEXT_PUBLIC_을 붙여야 합니다.
"이건 공개되어도 안전한 키야"라고 Next.js에게 알려주는 거죠.
# .env.local
NEXT_PUBLIC_WEATHER_API_KEY=abcdef123456
console.log(process.env.NEXT_PUBLIC_WEATHER_API_KEY); // 결과: abcdef123456
이제 잘 나옵니다.
주의: Google Analytics ID나 Firebase Public Key 같은 것만 이렇게 써야 합니다.
서버 비밀 키에 NEXT_PUBLIC_을 붙이는 순간, 여러분의 서버는 공공재가 됩니다.
4. 해결책 2 - 진짜 안전한 방법 (API Routes)
사실 API 키는 클라이언트에 노출하지 않는 게 가장 좋습니다. 아무리 Public Key라 해도, 누군가 내 키를 훔쳐서 무단으로 사용하면 요금 폭탄을 맞을 수 있거든요.
가장 좋은 방법은 Next.js API Route를 프록시(Proxy)로 쓰는 겁니다.
- 브라우저: "날씨 정보 줘" -> 내 서버 (/api/weather)
- 내 서버: (비밀 키 붙여서) -> OpenWeatherMap
- 내 서버: (결과만) -> 브라우저
/* src/app/api/weather/route.ts */
export async function GET() {
// 여기는 서버라서 접두사 없이도 읽을 수 있음!
const apiKey = process.env.WEATHER_API_KEY;
const res = await fetch(`https://api.weather.com...?key=${apiKey}`);
const data = await res.json();
return Response.json(data);
}
이렇게 하면 API 키는 서버에 안전하게 숨겨지고, 브라우저는 키의 존재조차 모르게 됩니다.
빌드 타임 vs 런타임 (인라이닝의 비밀) 더 알아보기
많은 분들이 오해하는 게 있습니다.
"브라우저에서 process.env를 읽는구나!"
아닙니다. 브라우저에는 process라는 객체 자체가 없습니다. (이건 Node.js 거니까요.)
그럼 어떻게 읽히는 걸까요?
인라이닝 (Inlining)
Next.js는 빌드 타임(Build Time)에 코드를 스캔합니다.
그리고 process.env.NEXT_PUBLIC_KEY라는 문자열을 발견하면, 그걸 실제 값으로 바꿔치기(Replace) 해버립니다.
작성한 코드:
console.log(process.env.NEXT_PUBLIC_API_URL);
빌드된 결과물 (브라우저가 받는 파일):
console.log("https://api.example.com");
이게 바로 "인라이닝"입니다. 그래서 빌드할 때 환경 변수가 없으면, 브라우저에서는 영원히 undefined가 뜹니다. Docker로 배포할 때, 이미지를 빌드하는 시점(CI/CD)에 환경 변수를 안 넣고, 컨테이너 실행 시점(Runtime)에 넣으면 값이 비어있는 이유가 바로 이겁니다.
Docker 배포 시 주의사항
Next.js의 Standalone 모드를 쓰거나 Docker를 쓴다면, NEXT_PUBLIC_ 변수는 빌드 타임에 확정된다는 걸 명심해야 합니다.
런타임에 동적으로 바꾸고 싶다면? window.ENV 같은 전역 객체를 주입하거나, 별도의 API를 통해 설정을 받아오는 복잡한 방법을 써야 합니다.
6. 마무리 - undefined는 당신을 지키고 있다
process.env가 undefined라고 화내지 마세요.
Next.js가 여러분의 실수로 데이터베이스가 털리는 걸 막아준 겁니다.
- Server(터미널)에서는 다 읽을 수 있다. (비밀 키는 여기서만 쓰자)
- Client(브라우저)에서는
NEXT_PUBLIC_만 읽을 수 있다. (이건 빌드 타임에 박제된다) - 진짜 중요한 키는 절대
NEXT_PUBLIC_붙이지 말고, API Route 뒤에 숨겨라. (프록시 패턴)
이 3가지만 기억하면, 환경 변수 때문에 3시간을 날리는 일은 없을 겁니다.
Why is My API Key undefined? (The Danger of Leaking Secrets in Next.js)
1. "I Swear I Put It into .env..."
I was building a weather app with Next.js.
I got an OpenWeatherMap API key and carefully placed it in .env.
# .env.local
WEATHER_API_KEY=abcdef123456
Then I confidently logged it in my component.
// WeatherComponent.tsx
'use client';
console.log(process.env.WEATHER_API_KEY); // Result: undefined
"Huh? Is it a typo?"
I restarted the server, cleared the cache, rebooted my Mac. Still undefined.
Strangely, console.log printed fine in the Terminal (Server), but not in the Browser Console.
2. Next.js's "Security Firewall"
It turned out, this wasn't a bug. It was a Security Feature.
The browser runs on the user's computer. Anyone can press F12 and inspect the code.
If all variables in process.env were sent to the browser?
AWS_SECRET_KEYLeakedDB_PASSWORDLeaked- Hacker: "Thanks for the free server."
So Next.js defaults to keeping ALL environment variables on the Server Side only. It only sends variables to the Browser (Client Side) if explicitly allowed. This is the implementation of the Server-Client Boundary principle.
3. Solution 1: "Hey, It's Safe to Share" (NEXT_PUBLIC_)
If you want to access a variable in the browser, you must prefix it with NEXT_PUBLIC_.
This tells Next.js, "It's okay to expose this key."
# .env.local
NEXT_PUBLIC_WEATHER_API_KEY=abcdef123456
console.log(process.env.NEXT_PUBLIC_WEATHER_API_KEY); // Result: abcdef123456
Now it works.
Warning: Only use this for Google Analytics IDs or Firebase Public Keys.
If you put NEXT_PUBLIC_ on a Server Secret, your server becomes public property.
4. Solution 2: The Truly Secure Way (API Routes)
Ideally, you shouldn't expose API keys to the client at all. Even if it's a "Public" key, someone could steal it and exhaust your quota, causing a billing spike.
The best practice is to use Next.js API Routes as a Proxy.
- Browser: "Get Weather" -> My Server (/api/weather)
- My Server: (Attaches Secret Key) -> OpenWeatherMap
- My Server: (Returns Result) -> Browser
/* src/app/api/weather/route.ts */
export async function GET() {
// Server-side: Can read without prefix!
const apiKey = process.env.WEATHER_API_KEY;
const res = await fetch(`https://api.weather.com...?key=${apiKey}`);
const data = await res.json();
return Response.json(data);
}
This way, the API Key stays safely on the server, and the browser never even knows it exists.
5. Deep Dive: Build Time vs Runtime (The Secret of Inlining)
Many developers misunderstand how this works.
"Oh, so the browser reads process.env from the server?"
No. The browser doesn't have a process object (that's a Node.js thing).
So how does it read the value?
The Inlining Mechanism
Next.js scans your code at Build Time.
When it sees process.env.NEXT_PUBLIC_KEY, it literally Finding & Replacing it with the string value.
Code you wrote:
console.log(process.env.NEXT_PUBLIC_API_URL);
Code bundled to browser:
console.log("https://api.example.com");
This is called Inlining.
Because of this, if the environment variable is missing at Build Time, it will remain undefined forever in the browser.
The Docker Trap
This is a classic trap when using Docker.
- You build the Docker image (CI/CD). You don't pass
.envhere because you think "I'll pass it when I run the container." - Next.js builds the app. It sees
undefinedforNEXT_PUBLIC_API_URLbecause the variable wasn't there. It hardcodesundefinedinto the JS bundle. - You run the container with
docker run -e NEXT_PUBLIC_API_URL=.... - Too late! The JS bundle already has
undefinedbaked in.
Solution for Docker:
- Method A: Provide build-time arguments (
ARG) in Dockerfile. - Method B: Use "Runtime Configuration" (e.g., expose env vars via a specialized API endpoint or inject them into
windowobject inlayout.tsx).
6. Type-Safe Environment Variables (T3 Env)
If you are tired of debugging undefined, I highly recommend using t3-env or just standard Zod validation.
Don't just trust process.env. Validate it.
// env.mjs
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
export const env = createEnv({
server: {
DATABASE_URL: z.string().url(),
OPEN_AI_API_KEY: z.string().min(1),
},
client: {
NEXT_PUBLIC_PUBLISHABLE_KEY: z.string().min(1),
},
runtimeEnv: {
DATABASE_URL: process.env.DATABASE_URL,
NEXT_PUBLIC_PUBLISHABLE_KEY: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY,
// ...
},
});
If you use this, your app will crash immediately at build time if a key is missing, instead of silently failing with undefined in production.
A loud error during build is 100x better than a silent bug in production.
7. Conclusion: undefined is Protecting You
Don't get mad that process.env is undefined.
Next.js just saved you from leaking your database password to the entire world.
Key Takeaways:
- Server (Terminal) sees everything. It has full access to the underlying operating system's environment variables.
- Client (Browser) only sees
NEXT_PUBLIC_. This is a safety mechanism. Warning: These values are Inlined at build time. - Hide real secrets behind API Routes. Never use
NEXT_PUBLIC_for database credentials or private API keys. Always proxy these requests.
This "Server-Client Boundary" is one of the most important concepts in modern web development. It might feel like an obstacle when you are just trying to get a simple API key to work, but it is the firewall that keeps your application secure.
Remember these rules, and you won't waste 3 hours debugging environment variables again. Next time you see undefined, smile and thank Next.js for watching your back.