process.env 완전 정복 — Next.js 환경변수 한 단계씩

⚙️ process.env 완전 정복 — Next.js 환경변수 한 단계씩
본 튜토리얼의 app/books/page.tsx 에 이런 한 줄이 있어요:
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3300";
process.env 가 뭐고, 왜 NEXT_PUBLIC_ 가 붙어 있고, ?? 는 뭘 의미하는지 — 이 한 줄에 들어 있는 모든 걸 CLI 실행 과 실제 페이지 두 가지로 직접 확인하면서 익힙니다.
STEP 1 process 가 뭐지? ← CLI STEP 2 process.env 살펴보기 ← CLI STEP 3 내가 변수 끼워 넣어 보기 ← CLI STEP 4 ?? 로 기본값 ← CLI STEP 5 .env.local 파일 만들기 ← Next.js STEP 6 NEXT_PUBLIC_ 접두사 의미 ← 페이지 STEP 7 /books/page.tsx 의 그 한 줄 ← 정리
사전 준비: 본 프로젝트(
book-api-tutorial) 에 이미tsx가 설치돼 있다고 가정합니다. (없으면npm install -D tsx)
STEP 1. process 가 뭐지?
process 는 Node.js 가 제공하는 빌트인 객체 입니다. 우리 코드가 실행되고 있는 그 Node 프로세스 자체에 대한 정보를 담고 있어요.
- 어떤 버전의 Node 에서 실행되고 있는가?
- 운영체제는 뭔가?
- 명령행에 어떤 인자가 들어왔는가?
- 환경변수는 뭐가 있는가? ← 우리가 보고 싶은 거
📄 scripts/test-env.ts 를 새로 만듭니다:
console.log("typeof process:", typeof process); console.log("process.version:", process.version); console.log("process.platform:", process.platform);
실행:
npx tsx scripts/test-env.ts
출력 예:
typeof process: object process.version: v24.12.0 process.platform: darwin
풀어 보기
🔹 typeof process 가 "object" — 즉 process 는 그냥 거대한 객체 하나입니다. 그 안에 정보가 필드로 잔뜩 들어 있어요.
🔹 process.version — 지금 실행 중인 Node 버전.
🔹 process.platform — darwin (macOS), linux, win32 같은 OS 식별자.
💡 주의:
process는 Node.js 만의 것 입니다. 브라우저에는 없어요. 그래서 클라이언트 컴포넌트("use client")에서process를 그대로 쓰면 동작이 다르거나 빌드 도구가 흉내 내 줍니다. 이 차이는 STEP 6 에서 다룹니다.
STEP 2. process.env 살펴보기
process 객체의 수많은 필드 중 하나가 process.env 입니다. 환경변수의 모음 이에요. 환경변수 (Environment Variable) 는 운영체제나 쉘이 우리 프로그램한테 던져 주는 "외부 설정값" 입니다.
📄 scripts/test-env.ts 를 다음으로 교체:
console.log("\n=== process.env 는 환경변수들의 모음 ==="); console.log("타입:", typeof process.env); const keys = Object.keys(process.env); console.log("내 시스템 환경변수 개수:", keys.length); console.log("처음 5개 키:", keys.slice(0, 5)); console.log("\n=== 자주 보는 시스템 환경변수 ==="); console.log("HOME:", process.env.HOME); console.log("USER:", process.env.USER); console.log("PATH(앞 60자):", process.env.PATH?.slice(0, 60), "...");
실행:
npx tsx scripts/test-env.ts
출력 예:
=== process.env 는 환경변수들의 모음 === 타입: object 내 시스템 환경변수 개수: 94 처음 5개 키: [ 'NVM_INC', 'TERM_PROGRAM', 'NODE', ... ] === 자주 보는 시스템 환경변수 === HOME: /Users/toto USER: toto PATH(앞 60자): /Users/toto/devel/myprojects/mywork/book-api-tutorial/no ...
풀어 보기
🔹 process.env 도 객체 입니다. 키는 모두 대문자 + 언더스코어 관례를 따르고, 값은 항상 문자열 (또는 undefined).
🔹 시스템 변수 가 잔뜩 들어 있어요. HOME, USER, PATH 같은 건 우리가 따로 정의한 적 없는데 운영체제가 알아서 넣어 준 겁니다. 셸을 열 때 자동으로 설정되는 값들.
🔹 process.env.PATH?.slice(0, 60) 의 ?. — process.env.PATH 가 undefined 일 수도 있으니, 그러면 .slice 를 호출하지 말고 그대로 undefined 를 돌려달라는 안전한 접근법.
✅ 확인 포인트
내 시스템에 어떤 환경변수가 있는지 직접 한 번 둘러봤다면 OK.
STEP 3. 내가 직접 변수 끼워 넣어 보기
환경변수는 명령 앞에 붙여서 일회용으로 정할 수도 있습니다. 이건 자주 쓰는 패턴이에요.
📄 scripts/test-env.ts 끝에 추가:
console.log("\n=== 우리가 직접 넣은 변수 GREETING ==="); console.log("값:", process.env.GREETING); console.log("타입:", typeof process.env.GREETING); console.log("\n=== 없는 변수는 undefined ==="); console.log("DOES_NOT_EXIST:", process.env.DOES_NOT_EXIST);
첫 번째 실행 — GREETING 없이
npx tsx scripts/test-env.ts
출력:
=== 우리가 직접 넣은 변수 GREETING === 값: undefined 타입: undefined === 없는 변수는 undefined === DOES_NOT_EXIST: undefined
두 번째 실행 — GREETING 끼워서
GREETING=안녕하세요 npx tsx scripts/test-env.ts
출력 (마지막 부분):
=== 우리가 직접 넣은 변수 GREETING === 값: 안녕하세요 타입: string
풀어 보기
🔹 이름=값 명령어 문법
명령 앞에 붙은 GREETING=안녕하세요 는 "이 명령을 실행하는 동안만 GREETING 이라는 환경변수를 이 값으로 설정해" 라는 뜻입니다. 셸에 영구 등록되는 게 아니라 그 한 번의 명령에만 적용돼요.
🔹 process.env.X 의 타입
값이 있으면 string, 없으면 undefined. 환경변수는 무조건 문자열이라서, 숫자가 필요하면 Number(process.env.X) 처럼 변환해야 합니다.
💡 여러 개를 한꺼번에:
FOO=1 BAR=2 npx tsx scripts/test-env.ts
✅ 확인 포인트
같은 명령에 변수를 끼웠을 때와 안 끼웠을 때 결과가 달라지는 걸 확인했나요?
STEP 4. ?? 로 기본값 주기
환경변수가 없을 수도 있는 상황에서, 없으면 무엇을 쓸지 미리 적어 두는 패턴.
📄 scripts/test-env.ts 끝에 추가:
console.log("\n=== ?? 로 기본값 주기 ==="); const greeting = process.env.GREETING ?? "안녕(기본값)"; console.log("greeting:", greeting);
실행
# 1) 변수 없을 때 npx tsx scripts/test-env.ts # → greeting: 안녕(기본값) # 2) 변수 있을 때 GREETING=반가워요 npx tsx scripts/test-env.ts # → greeting: 반가워요
풀어 보기
🔹 A ?? B ("Nullish Coalescing")
"A 가 null 이거나 undefined 면 B 를, 아니면 A 를 써라" 라는 의미.
process.env.X 는 값이 없으면 undefined 라서, ?? 와 짝궁입니다.
🔹 A || B 와 다른 점
A || B 는 A 가 "빈 문자열""", 0, false 일 때도 B 를 씁니다. 환경변수에서는 빈 문자열이 의도된 값일 수도 있으니 ?? 가 더 안전해요.
process.env.FOO || "기본" // FOO 가 빈 문자열이면 "기본" 으로 — 의도 어긋날 수 있음 process.env.FOO ?? "기본" // FOO 가 undefined 일 때만 "기본"
✅ 확인 포인트
이제 우리 app/books/page.tsx 의 process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3300" 의 모양이 익숙해졌을 거예요. STEP 7 에서 다시 봅니다.
STEP 5. .env.local — Next.js 가 자동으로 읽어 주는 파일
매번 FOO=bar npx tsx ... 처럼 적는 건 귀찮습니다. 프로젝트 단위로 미리 정의 해 두는 게 더 좋아요. Next.js 는 프로젝트 루트의 .env.local 파일을 시작할 때 자동으로 읽어서 process.env 에 채워 줍니다.
5-1. .env.local 만들기
프로젝트 루트(= package.json 옆) 에 .env.local 파일을 새로 만듭니다:
# .env.local NEXT_PUBLIC_DEMO_NAME=책 관리 튜토리얼 (PUBLIC) SECRET_API_KEY=super-secret-do-not-leak
💡 변수 이름 두 개를 일부러 다르게 지었어요. 하나는
NEXT_PUBLIC_접두사가 붙고, 하나는 안 붙습니다. 이 차이가 STEP 6 의 핵심.
5-2. 확인용 API 라우트 만들기
📄 app/api/env-test/route.ts:
import { NextResponse } from "next/server"; export async function GET() { return NextResponse.json({ "NEXT_PUBLIC_DEMO_NAME (.env.local)": process.env.NEXT_PUBLIC_DEMO_NAME, "SECRET_API_KEY (.env.local)": process.env.SECRET_API_KEY, "NODE_ENV": process.env.NODE_ENV, "HOME (시스템)": process.env.HOME, }); }
5-3. dev 서버 (재)시작
.env.local 을 처음 만들거나 수정했다면 dev 서버를 재시작 해야 합니다. (실행 중에 만들면 다음 요청부터 자동 반영되지만, 동작이 이상하다 싶으면 재시작이 안전합니다.)
npm run dev
콘솔 출력에 이런 줄이 보이면 정상:
- Environments: .env.local
5-4. 확인
curl http://localhost:3300/api/env-test
출력:
{ "NEXT_PUBLIC_DEMO_NAME (.env.local)": "책 관리 튜토리얼 (PUBLIC)", "SECRET_API_KEY (.env.local)": "super-secret-do-not-leak", "NODE_ENV": "development", "HOME (시스템)": "/Users/toto" }
.env.local 의 두 변수가 모두 보입니다. 그리고 시스템 변수 (HOME) 도 그대로 보여요. 즉 Next.js 가 .env.local 을 읽어 시스템 환경변수와 합쳐서 process.env 를 만들어 줍니다.
💡
NODE_ENV는 Next.js 가 자동으로 채우는 변수예요.npm run dev면development,npm run build/npm start면production.
5-5. 환경변수 파일의 종류
Next.js 는 다음 파일들을 인식합니다 (우선순위 높은 순):
| 파일 | 언제 쓰나 |
|---|---|
.env.development.local, .env.production.local | 환경별 + 개인용 (커밋 X) |
.env.local | 전 환경 공통 + 개인용 (커밋 X) |
.env.development, .env.production | 환경별 + 팀 공유 (커밋 O) |
.env | 전 환경 공통 + 팀 공유 (커밋 O) |
가장 자주 쓰는 건 .env.local 입니다. 보통 .gitignore 에 들어 있어 비밀이 실수로 git 에 올라가지 않아요.
✅ 확인 포인트
curl http://localhost:3300/api/env-test가 두 값을 잘 돌려주나요?.env.local의 값을 한 글자 바꾸고 dev 서버를 재시작 한 뒤 다시 curl 해서 값이 바뀌나요?
STEP 6. NEXT_PUBLIC_ 접두사의 의미
여기가 가장 중요한 부분입니다.
한 줄로: NEXT_PUBLIC_ 으로 시작하는 환경변수만 브라우저에 보내져 클라이언트 코드에서도 읽을 수 있습니다. 그 외 변수는 서버에서만 보입니다.
이유: 안전. API 키나 DB 비밀번호 같은 걸 환경변수에 두는데, 그게 의도치 않게 브라우저로 새면 큰일 나니까요. Next.js 가 기본을 "보내지 않음" 으로 두고, 개발자가 명시적으로 NEXT_PUBLIC_ 접두사를 붙여야만 보내 줍니다.
6-1. 서버 / 클라이언트 비교 페이지
📄 app/env-test/page.tsx (서버 컴포넌트):
import Link from "next/link"; import ClientEnvInfo from "./ClientEnvInfo"; export const dynamic = "force-dynamic"; export default function EnvTestPage() { const serverPublic = process.env.NEXT_PUBLIC_DEMO_NAME ?? "(undefined)"; const serverSecret = process.env.SECRET_API_KEY ?? "(undefined)"; return ( <main className="mx-auto max-w-2xl px-6 py-12 space-y-6"> <Link href="/" className="text-sm text-amber-600 hover:underline"> ← 홈으로 </Link> <h1 className="text-2xl font-black">🔬 환경변수 확인 페이지</h1> <section className="rounded-lg border border-emerald-200 bg-emerald-50 p-4"> <h2 className="text-lg font-bold text-emerald-900"> 🖥️ 서버 컴포넌트에서 본 환경변수 </h2> <dl className="mt-3 space-y-2 text-sm"> <div> <dt className="font-semibold text-stone-700">NEXT_PUBLIC_DEMO_NAME:</dt> <dd className="font-mono text-stone-900">{serverPublic}</dd> </div> <div> <dt className="font-semibold text-stone-700">SECRET_API_KEY:</dt> <dd className="font-mono text-stone-900">{serverSecret}</dd> </div> </dl> </section> <ClientEnvInfo /> </main> ); }
📄 app/env-test/ClientEnvInfo.tsx (클라이언트 컴포넌트):
"use client"; import { useEffect, useState } from "react"; export default function ClientEnvInfo() { const [publicValue, setPublicValue] = useState<string | undefined | "loading">( "loading", ); const [secretValue, setSecretValue] = useState<string | undefined | "loading">( "loading", ); useEffect(() => { // 이 코드는 브라우저에서만 실행됩니다. setPublicValue(process.env.NEXT_PUBLIC_DEMO_NAME); setSecretValue(process.env.SECRET_API_KEY); }, []); return ( <section className="rounded-lg border border-sky-200 bg-sky-50 p-4"> <h2 className="text-lg font-bold text-sky-900"> 🌐 클라이언트 컴포넌트(브라우저)에서 본 환경변수 </h2> <dl className="mt-3 space-y-2 text-sm"> <div> <dt className="font-semibold text-stone-700">NEXT_PUBLIC_DEMO_NAME:</dt> <dd className="font-mono text-stone-900"> {publicValue === "loading" ? "(브라우저에서 계산 중…)" : (publicValue ?? "(undefined)")} </dd> </div> <div> <dt className="font-semibold text-stone-700">SECRET_API_KEY:</dt> <dd className="font-mono text-stone-900"> {secretValue === "loading" ? "(브라우저에서 계산 중…)" : (secretValue ?? "(undefined ← 의도된 동작)")} </dd> </div> </dl> </section> ); }
6-2. 브라우저로 확인
http://localhost:3300/env-test 로 접속합니다.
화면에는 두 영역이 보입니다.
| 영역 | NEXT_PUBLIC_DEMO_NAME | SECRET_API_KEY |
|---|---|---|
| 🖥️ 서버 컴포넌트 | "책 관리 튜토리얼 (PUBLIC)" | "super-secret-do-not-leak" |
| 🌐 클라이언트 컴포넌트 (브라우저) | "책 관리 튜토리얼 (PUBLIC)" | "(undefined ← 의도된 동작)" |
같은 환경변수 두 개를, 같은 페이지에서 서버 컴포넌트와 클라이언트 컴포넌트가 다르게 보는 거예요. 클라이언트는 NEXT_PUBLIC_ 만 보입니다.
6-3. 브라우저 번들에 진짜 secret이 없는지 확인
진짜 secret이 브라우저로 전송 안 됐는지 확인할 수도 있어요. 페이지를 한 번 불러온 뒤 터미널에서:
grep -r "super-secret-do-not-leak" .next/static 2>/dev/null
아무것도 안 나옵니다. 즉 빌드된 브라우저 JavaScript 안 어디에도 secret 문자열이 들어 있지 않아요. Next.js 가 NEXT_PUBLIC_ 접두사를 검사해서, 그게 없는 변수는 클라이언트 번들에 절대 포함하지 않습니다.
반대로 NEXT_PUBLIC_DEMO_NAME 값은 페이지를 한 번 로드해 JavaScript 가 만들어진 뒤:
grep -l "책 관리 튜토리얼" .next/static/chunks/app/env-test/*.js 2>/dev/null
여기엔 들어 있습니다. 빌드 시 코드에 박혀 브라우저로 전송돼요.
6-4. ⚠️ 함정: 클라이언트 컴포넌트도 SSR 단계에서는 서버에서 돈다
이건 살짝 어려운 주제지만 매우 중요합니다.
"use client" 가 붙어 있어도, 첫 페이지 응답 을 만들 때는 그 컴포넌트가 서버(Node) 에서 한 번 실행됩니다 (SSR). 이때 Node 의 process.env 는 모든 변수를 다 갖고 있어요.
그래서 만약 ClientEnvInfo 를 이렇게 짰다면:
// ❌ 위험한 패턴 export default function ClientEnvInfo() { return <p>{process.env.SECRET_API_KEY}</p>; }
- SSR 단계 (Node): SECRET 의 실제 값이 HTML 에 박힘
- 브라우저에 도착한 HTML: secret 값이 그대로 보임. 누설!
- 하이드레이션 시 (브라우저에서 다시 그리기): process.env.SECRET 은 undefined → 화면이 갑자기 바뀜 (hydration mismatch)
그래서 안전한 패턴은 우리가 쓴 것처럼 useEffect 안에서만 process.env 를 만지는 거예요. useEffect 는 브라우저에서만 실행되므로 process.env 도 브라우저용 — 즉 NEXT_PUBLIC_ 만 들어 있는 객체입니다.
🔑 한 줄 규칙: 클라이언트 컴포넌트에서
NEXT_PUBLIC_가 아닌 환경변수는 절대로 쓰지 말 것. 정 써야 하면 useEffect 안에서 — 그래도 의미가 없다는 사실은 알고 쓰기.
✅ 확인 포인트
- 페이지의 서버 영역과 클라이언트 영역 값이 다른가요?
.next/static에 secret 문자열이 없나요?
STEP 7. app/books/page.tsx 의 그 한 줄로 돌아가기
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3300";
이제 이 한 줄을 완전히 해석할 수 있어요.
| 부분 | 의미 |
|---|---|
process.env | 환경변수 모음 객체 |
.NEXT_PUBLIC_BASE_URL | "기본 URL" 이라는 환경변수. NEXT_PUBLIC_ 라서 클라이언트에도 노출 가능. |
?? | 값이 없으면 오른쪽을 쓰겠다 |
"http://localhost:3300" | 폴백 — 로컬 개발용 기본값 |
왜 이렇게 짰을까?
이 코드는 서버 컴포넌트 안에서 fetch 를 부릅니다:
const res = await fetch(`${baseUrl}/api/books`, { cache: "no-store" });
서버 컴포넌트의 fetch 는 Node 에서 실행되니, 절대 URL 이 필요해요 ("어떤 도메인 기준의 상대 경로" 라는 정보가 없으니까).
- 로컬 개발:
localhost:3300으로 충분 → 폴백 값이 그대로 쓰임. - Vercel 등 배포 환경: 도메인이
https://my-board.vercel.app같은 거 → Vercel 대시보드에서NEXT_PUBLIC_BASE_URL=https://my-board.vercel.app로 설정. 코드는 그걸 자동으로 집어 옵니다. 코드 한 줄 안 고치고 환경별로 다른 URL 을 쓰는 것 이 이 패턴의 진짜 가치예요.
💡 그럼 왜 굳이
NEXT_PUBLIC_접두사? 사실 이 코드는 서버에서만 도니까 접두사가 없어도 동작합니다. 그래도 붙인 이유는, 같은 변수를 나중에 클라이언트 컴포넌트(예: 로그인 페이지)에서도 쓰고 싶을 때 한 번 더 정의하지 않기 위해서예요. "URL 처럼 누가 봐도 괜찮은 값"이라 접두사 붙이는 게 안전.
실험: 다른 값으로 덮어쓰기
.env.local 에 추가:
NEXT_PUBLIC_BASE_URL=http://localhost:9999
dev 서버를 재시작한 뒤 http://localhost:3300/books 에 접속하면 — 페이지가 안 뜹니다. 콘솔에 "fetch failed" 같은 에러가 떠요. 이유: 우리 코드가 9999 번 포트로 API를 부르려는데 그 포트엔 아무도 없거든요.
이 줄을 다시 지우거나 http://localhost:3300 으로 고치면 정상 동작. 환경변수가 진짜로 동작에 영향을 주는 걸 직접 본 거예요.
정리
| 의문 | 답 |
|---|---|
process 가 뭐지? | Node.js 의 빌트인 객체. 실행 중 프로세스 정보 담음. |
process.env 는? | 그 프로세스가 갖고 있는 모든 환경변수의 모음 객체. 값은 모두 문자열 또는 undefined. |
| 환경변수는 어디서 옴? | (1) OS / 셸, (2) 명령 앞에 인라인으로 (FOO=bar cmd), (3) .env.local 같은 파일 (Next.js 가 로드). |
?? 는? | "왼쪽이 null/undefined 면 오른쪽" — 환경변수 폴백에 잘 어울림. |
.env.local 은? | Next.js 가 자동으로 읽는 비공개 환경변수 파일. .gitignore 에 들어가 있어 보안 안전. |
NEXT_PUBLIC_ 접두사? | 이걸 붙여야 클라이언트 코드에서도 값을 볼 수 있음. 안 붙이면 서버 전용. |
| 클라이언트 컴포넌트 함정 | "use client" 라도 SSR 단계에선 서버에서 도므로, 거기서 process.env.SECRET 을 쓰면 HTML 로 누설. useEffect 안에서만 만질 것. |
산출물 정리
이 튜토리얼에서 만든 학습용 파일들은 본 프로젝트에 영향을 주지 않습니다. 다만 정리하고 싶으면:
rm -rf scripts/test-env.ts rm -rf app/api/env-test rm -rf app/env-test # .env.local 은 의도에 맞으면 남겨도 됩니다. # 단 NEXT_PUBLIC_BASE_URL 을 정의했다면 다시 지우거나 정상 URL 로 두세요.
다음 단계
zod로 환경변수 검증: 운영에서 환경변수가 비어 있으면 빌드 시 바로 에러로 막는 패턴.- 여러 환경 변수 파일:
.env,.env.production등을 함께 써서 개발/배포 값을 분리. - 시크릿 관리 서비스: AWS Secrets Manager, Vercel Environment Variables 등 외부 보관소에서 비밀을 가져오는 방식.
축하해요. 이제 process.env.NEXT_PUBLIC_BASE_URL ?? "http://localhost:3300" 이 무엇을 하는지 한 단어도 빠짐없이 설명할 수 있게 됐습니다 ⚙️

댓글
댓글을 작성하려면 이 필요합니다.