C.W.K.
Stream
Lesson 03 of 07 · published

Env Modes — 시크릿 안 새는 .env 파일

~10 min · env, vite, modes, secrets

Level 0React 입문자
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
Repo 의 모든 환경 변수가 부주의한 commit 한 번이면 public. Vite 의 env 모델은 일부러 보수적: 명시적 prefix, 레이어된 파일, 빌드 시 치환.

파일 계층

  • .env — 모든 모드에 로드. 비시크릿 디폴트엔 commit.
  • .env.local — 모든 모드에 로드, gitignored. 로컬 override.
  • .env.development — dev 에만 로드. Dev-안전 값 commit.
  • .env.production — build/preview 에만 로드.
  • .env.[mode].local — gitignored 모드-specific override.

Vite 가 순서대로 머지; 뒤 파일이 앞 거 override.

VITE_ prefix

VITE_ 로 시작하는 env var 만 import.meta.env.VITE_X 통해 클라이언트 코드에 inline. 다른 모든 것은 번들 밖 — 참조해도. 이건 정적 분석: prefix 가 본인 안전 레일.

빌드 시 치환

Vite 가 import.meta.env.VITE_X 를 빌드 시 실제 문자열로 교체. 변수 값이 JS 에 구워짐. 브라우저에 런타임 env lookup 없음. 두 결과:

  • 값 변경에 rebuild 필요.
  • 런타임만 의도한 시크릿 값은 절대 VITE_ prefix 쓰면 안 됨 — JS 에 있고 다운로드한 누구든 읽음.

런타임 config 는?

Rebuild 없이 배포마다 다른 config 필요하면 (멀티-테넌트, 지역-specific endpoint), 백엔드에서 런타임에 config fetch 또는 배포 시 HTML 통해 window.__APP_CONFIG__ 에 inject. Env var 는 빌드 시 디폴트용; 런타임 config 는 다른 문제.

클라이언트 번들에 있으면 public. 어떤 VITE_-prefixed 변수에도 DB URL, API 시크릿, auth 토큰 두지 마. JS 파일이 plaintext. 누구든 view-source.

Code

.env 파일 — 어디에 뭐 가는지·bash
# .env (committed) — 알아도 안전한 디폴트
VITE_APP_NAME=My App
VITE_API_URL=/api

# .env.development.local (gitignored) — dev 전용 override, build 엔 안 들어감
VITE_API_URL=http://localhost:8000

# .env.production (committed) — prod-only 안전 디폴트
VITE_API_URL=https://api.myapp.com

# .env.production.local (gitignored) — 배포-specific
VITE_SENTRY_DSN=https://...@sentry.io/...

# 서버 전용 시크릿 — VITE_ prefix 없음, 절대 어떤 클라이언트 번들에도 안 됨
DATABASE_URL=postgres://...
ADMIN_TOKEN=secret
코드에서 env var 읽기·tsx
// import.meta.env.VITE_X — string 타입 (또는 미설정 시 undefined).
const apiUrl: string = import.meta.env.VITE_API_URL;

// src/vite-env.d.ts 의 선언으로 타입-안전:
interface ImportMetaEnv {
  readonly VITE_API_URL: string;
  readonly VITE_APP_NAME: string;
  readonly VITE_SENTRY_DSN?: string;
}
interface ImportMeta {
  readonly env: ImportMetaEnv;
}

// 빌트인 모드:
const isDev = import.meta.env.DEV;       // dev 에서 true
const isProd = import.meta.env.PROD;     // build/preview 에서 true
const mode = import.meta.env.MODE;       // 'development' | 'production' | custom

External links

Exercise

Bootstrap 프로젝트에서: VITE_APP_NAMEVITE_API_URL 가진 .env 생성. 다른 VITE_API_URL 가진 .env.development.local 생성. import.meta.env 통해 App.tsx 에 둘 다 사용. Dev 는 .env.development.local 값 보여주고 npm run build.env 값으로 fallback 하는지 확인 — .development infix 가 override 를 dev 에만 스코프. 그냥 .env.local 은 모든 모드(build 포함)에 로드돼서 production build 에도 override 가 남고 verify-step 이 재현 안 됨. 그다음 Non-VITE_ 변수 (SECRET=hi) 추가 + 빌드된 번들에 절대 안 나타나는지 확인 (grep dist/assets/).
Hint
dist/assets/*.js grep 이 VITE_ 값들 반환하지만 SECRET 절대 아님. SECRET 나타나면 어디선가 import.meta.env.SECRET 으로 참조 — Vite 의 정적 체크가 막았어야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 피파warm

댓글 1

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.
  1. 이 thread는 leaf-bound Issue로 옮겨졌어요
    Chan
    Chan

    Hey guys, if you're doing the exercise, the instructions are actually a bit misleading based on how Vite works.

    The exercise says to use .env.local and expects the production build to fall back to .env. But in Vite, .env.local overrides EVERYTHING—both dev and build. So if you use .env.local, your production build will still end up with your localhost URL.

    To make it work the way the exercise actually wants you to, just do this:

    Change the local filename from .env.local to .env.development.local

    This forces the localhost URL to only load during npm run dev. Then when you run npm run build, it'll properly switch over to the production URL in your .env.

    Save yourselves the headache and change the filename!