본문 바로가기
C.W.K.
Stream
Lesson 03 of 07 · published

비밀을 노출하지 않는 Vite 환경 변수

~10 min · env, vite, modes, secrets

Level 0React 입문자
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
클라이언트 번들에 들어간 환경 변수는 공개 정보야. Vite의 prefix와 mode 규칙으로 빌드 설정과 비밀을 분리해.

파일마다 적용 범위가 달라

  • .env는 모든 mode에서 읽는 공개 기본값을 담아.
  • .env.local은 모든 mode에 적용되는 로컬 재정의이며 Git에 올리지 않아.
  • .env.development.env.production은 각각 개발과 프로덕션 빌드에 적용돼.
  • .env.[mode].local은 특정 mode에서만 쓰는 로컬 재정의야.

여러 파일에 같은 이름이 있으면 더 구체적으로 나중에 읽힌 값이 앞의 값을 덮어써.

브라우저에 보낼 값만 VITE_로 시작해

Vite는 이 접두사가 붙은 변수만 import.meta.env를 통해 클라이언트 코드에 넣어. 빌드할 때 실제 문자열로 치환되므로 내려받은 JavaScript를 보는 누구나 값을 읽을 수 있어.

값을 바꾸면 다시 빌드해야 해

브라우저가 실행 중에 서버의 환경 변수를 읽는 구조가 아니야. Endpoint처럼 빌드마다 달라지는 공개 설정에는 적합하지만 데이터베이스 비밀번호, API secret, 인증 토큰에는 절대 사용하면 안 돼.

런타임 설정은 별도 경로로 받아

재빌드 없이 tenant나 지역마다 값을 바꿔야 한다면 앱이 시작할 때 백엔드에서 config를 읽거나 배포 시 HTML의 전역 객체에 주입해. 빌드 설정과 런타임 설정은 서로 다른 문제야.

클라이언트 번들에 들어간 값은 공개된 값이야. 이름에 env가 붙었다고 비밀이 되지는 않아.

Mode별 파일의 우선순위

공통 .env를 먼저 읽고 local 파일과 현재 mode에 맞는 파일이 더 구체적인 값을 덮어써. .env.local.env.production.local처럼 이름에 local이 붙은 파일은 개인 환경과 배포 secret 저장소에서 만들고 Git에는 올리지 않아.

Vite가 제공하는 기본 값도 있어

import.meta.env.MODE는 현재 mode 이름을, DEV와 PROD는 개발·프로덕션 여부를 boolean으로 알려 줘. SSR은 서버 빌드 여부를 나타내. 직접 만든 값과 달리 이 이름에는 VITE_ 접두사가 필요하지 않아.

클라이언트 endpoint를 변수로 둘 때도 URL이 공개돼도 괜찮은지 확인해. 공개 API 주소와 secret credential은 다른 종류의 값이야. Secret은 브라우저로 보내지 말고 서버가 소유해야 해.

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
코드에서 환경 변수 읽기·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

.envVITE_APP_NAME과 공개 API URL을 넣고, .env.development.local에는 개발용 URL을 덮어써. 개발 서버에서는 개발 값이, npm run build와 preview에서는 production용 값이 보이는지 확인해. 이어서 VITE_ 접두사가 없는 가짜 secret과 접두사가 있는 가짜 secret을 각각 만들고, 어느 값이 dist/assets의 JavaScript에 들어가는지 검색해.
Hint
.env.local은 여러 mode에 적용되므로 이 비교에서는 사용하지 마. Dev 화면, production preview, 빌드 결과 문자열 검색까지 세 증거를 남기고, 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!