환경 변수가 undefined로 나올 때 (Vite)
1. 왜 이 문제를 만났나?
React 프로젝트를 CNA(Create React App)나 Webpack에서 Vite로 마이그레이션하거나, 처음 Vite를 세팅할 때 가장 흔하게 겪는 문제입니다.
.env 파일에 API 키를 넣었는데, 코드에서 찍어보면 undefined가 나옵니다.
// .env
API_KEY=my-secret-key
// App.tsx
console.log(process.env.API_KEY); // undefined!
console.log(import.meta.env.API_KEY); // undefined!
"분명 문법 맞는데 왜 안 되지?"라며 .env 파일 위치도 옮겨보고, 서버도 재시작해보지만 여전히 읽히지 않습니다.
이는 Vite가 환경 변수를 처리하는 기본 철학이 Node.js나 Webpack과는 다르기 때문입니다.
2. 핵심 원인 - process.env는 브라우저에 없다
가장 큰 오해는 "프론트엔드 코드에서 process.env를 쓸 수 있다"는 믿음입니다.
process객체는 Node.js 런타임에만 존재하는 전역 객체입니다.- 브라우저(Chrome, Safari) 환경에는
process라는 변수가 아예 없습니다.
그럼 CRA(Webpack)에서는 왜 됐을까?
CRA나 Webpack 기반 프로젝트에서는 빌드 도구가 소스 코드를 읽다가 process.env.REACT_APP_XXX를 발견하면, 빌드 시점에 그 자리에 실제 문자열 값을 때려 박아줍니다(Replacement).
즉, 브라우저가 process를 이해하는 게 아니라, 빌드 결과물에는 process가 사라지고 "실제값"만 남는 마법을 부린 것입니다.
Vite도 비슷한 마법을 부리지만, 주문(Syntax)이 다릅니다.
Vite는 최신 ES Modules 표준에 맞춰 import.meta.env라는 문법을 사용합니다.
3. 해결책 - 접두사(Prefix)와 문법 변경
Vite에서 환경 변수를 노출하려면 두 가지 규칙을 지켜야 합니다.
규칙 1 - VITE_ 접두사 붙이기
Vite는 기본적으로 .env 파일의 변수들을 클라이언트에 노출하지 않습니다. DB 비밀번호 같은 민감한 정보가 실수로 번들링되는 것을 막기 위함입니다.
클라이언트(브라우저)로 보내고 싶은 변수는 반드시 VITE_로 시작해야 합니다.
# ❌ 클라이언트에서 접근 불가능 (서버 사이드 전용)
DB_PASSWORD=secret1234
API_KEY=hidden-key
# ✅ 클라이언트에서 접근 가능
VITE_API_URL=https://api.myapp.com
VITE_ANALYTICS_ID=UA-12345678-1
규칙 2 - import.meta.env 사용하기
코드에서는 process.env 대신 import.meta.env 객체를 사용해야 합니다.
// App.tsx
// ❌ 작동 안 함
const apiUrl = process.env.VITE_API_URL;
// ✅ 올바른 사용법
const apiUrl = import.meta.env.VITE_API_URL;
console.log(`API Target: ${apiUrl}`);
4. 타입스크립트(TypeScript) 인텔리센스 추가하기 제대로 파보기
TypeScript를 쓴다면 import.meta.env.VITE_...를 칠 때 자동 완성이 안 돼서 불편할 수 있습니다.
이를 해결하려면 타입 정의 파일(d.ts)을 확장해야 합니다.
1단계 - vite-env.d.ts 생성
src 폴더에 vite-env.d.ts 파일을 만들고(또는 수정하고) 아래 내용을 추가합니다.
/// <reference types="vite/client" />
interface ImportMetaEnv {
// 여기에 여러분의 환경 변수를 정의하세요
readonly VITE_API_URL: string;
readonly VITE_APP_TITLE: string;
readonly VITE_FIREBASE_KEY: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
2단계 - tsconfig.json 확인
compilerOptions.types 배열에 "vite/client"가 포함되어 있는지 확인합니다.
{
"compilerOptions": {
"types": ["vite/client"]
}
}
이제 VS Code에서 import.meta.env.를 입력하면 정의한 변수들이 자동 완성 목록에 뜹니다!
5. 가이드 - 환경별 설정 (Staging vs Prod) 구분하기
실제로는 로컬(Local), 개발(Dev), 스테이징(Staging), 운영(Prod) 등 다양한 환경이 존재합니다.
Vite는 .env.mode 파일을 통해 이를 우아하게 지원합니다.
파일 우선순위 (아래로 갈수록 높은 우선순위)
.env(모든 경우에 로드됨).env.local(git ignored, 로컬 오버라이드용).env.[mode](특정 모드에서만 로드됨).env.[mode].local(특정 모드 로컬 오버라이드)
설정 예시
.env.development (로컬 개발용)
VITE_API_URL=http://localhost:8080
VITE_ENV_NAME=local
.env.staging (Q/A 테스트용)
VITE_API_URL=https://stg-api.myapp.com
VITE_ENV_NAME=staging
.env.production (실제 운영용)
VITE_API_URL=https://api.myapp.com
VITE_ENV_NAME=production
빌드 스크립트 수정 (package.json)
각 환경에 맞춰 빌드하려면 --mode 플래그를 사용합니다.
"scripts": {
"dev": "vite", // 기본적으로 .env.development 로드
"build": "vite build", // 기본적으로 .env.production 로드
"build:staging": "vite build --mode staging" // .env.staging 로드
}
이제 npm run build:staging을 실행하면 스테이징용 API 주소가 주입된 빌드 결과물이 나옵니다.
6. Docker와 런타임 환경 변수 (DevOps 필독)
여기서 많은 개발자가 좌절하는 포인트가 있습니다. "Vite의 환경 변수는 빌드 타임(Build Time)에 결정됩니다."
Docker 이미지를 한 번 빌드해서(Build Once), Dev/Staging/Prod 등 여러 환경에 배포(Deploy Anywhere)하려고 할 때 문제가 생깁니다.
이미 VITE_API_URL이 "http://localhost:8080"으로 박혀서 빌드된 도커 이미지를 운영 서버에 띄운다고 해서, 운영 서버의 환경 변수(VITE_API_URL=https://real-api.com)를 읽지 못합니다. 이미 HTML/JS 파일 안에 텍스트로 박제되었기 때문입니다.
해결책 - 런타임 주입 (Runtime Injection) 패턴
이 문제를 해결하려면 "window 객체 주입" 패턴을 써야 합니다.
-
public/config.js생성window.ENV = { API_URL: "DEFAULT_URL_FOR_DEV" }; -
index.html에서 로드<head> <script src="/config.js"></script> </head> -
App 코드에서 사용
// import.meta.env 대신 window.ENV 사용 const apiUrl = window.ENV?.API_URL || import.meta.env.VITE_API_URL; -
Docker Entrypoint 스크립트 작성 (
entrypoint.sh) 컨테이너가 시작될 때(docker run), 리눅스의sed명령어를 이용해config.js의 내용을 바꿔치기합니다.#!/bin/sh # config.js 파일의 내용을 현재 환경변수($API_URL)로 교체 sed -i "s|DEFAULT_URL_FOR_DEV|$API_URL|g" /usr/share/nginx/html/config.js # Nginx 실행 nginx -g "daemon off;"
이 패턴을 사용하면 하나의 도커 이미지로 로컬, 스테이징, 운영 환경 모두에 대응할 수 있습니다(12 Factor App 원칙 준수).
7. Monorepo 설정 (TurboRepo / Nx)
Monorepo를 사용 중이라면, 루트 디렉토리에 있는 .env를 하위 패키지(apps/web)에서 읽고 싶을 수 있습니다.
하지만 Vite는 기본적으로 프로젝트 루트(vite.config.ts가 있는 곳)의 .env만 읽습니다.
상위 폴더의 .env를 읽으려면 vite.config.ts를 수정해야 합니다.
import { defineConfig, loadEnv } from 'vite';
import path from 'path';
export default defineConfig(({ mode }) => {
// 현재 위치(__dirname)에서 두 단계 위(../../)를 환경 변수 루트로 설정
const env = loadEnv(mode, path.resolve(__dirname, '../../'), '');
return {
// 필요한 경우 define으로 명시적 주입도 가능
define: {
'import.meta.env.VITE_SHARED_KEY': JSON.stringify(env.VITE_SHARED_KEY)
}
};
});
이렇게 하면 루트 레벨의 공통 환경 변수를 모든 앱이 공유할 수 있습니다.
8. 요약 - 이것만 기억하세요
- 접두사 필수: 변수명 앞에
VITE_를 꼭 붙이세요. (VITE_API_URL) - 문법 변경:
process.env는 잊고import.meta.env를 쓰세요. - 서버 재시작:
.env파일 내용은 서버가 뜰 때 로드됩니다. 파일 수정 후엔 반드시 재시작 (npm run dev다시 실행) 하세요. - 타입 정의:
vite-env.d.ts에 타입을 추가하면 개발이 편해집니다. - 도커 배포 시: 빌드 타임 변수 말고,
window.ENV주입 패턴을 고려하세요.
Vite의 방식은 처음엔 낯설지만, 보안과 명시성 측면에서 훨씬 더 안전하고 현대적인 접근 방식입니다.