메인 콘텐츠로 바로가기
노트 목록

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

김성박600
목차9

⚙️ 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_NAMESECRET_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" 이 무엇을 하는지 한 단어도 빠짐없이 설명할 수 있게 됐습니다 ⚙️

댓글

불러오는 중...

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