
환경 변수가 undefined로 나올 때 (Vite): process.env는 잊으세요
Vite에서 환경 변수가 제대로 로드되지 않는 문제의 원인과 해결 방법을 심층 분석합니다. bundler의 동작 원리, 보안 모델, Docker/CI 환경에서의 동적 주입, 그리고 Monorepo 설정까지 완벽하게 가이드합니다.

Vite에서 환경 변수가 제대로 로드되지 않는 문제의 원인과 해결 방법을 심층 분석합니다. bundler의 동작 원리, 보안 모델, Docker/CI 환경에서의 동적 주입, 그리고 Monorepo 설정까지 완벽하게 가이드합니다.
분명히 클래스를 적었는데 화면은 그대로다? 개발자 도구엔 클래스가 있는데 스타일이 없다? Tailwind 실종 사건 수사 일지.

안드로이드는 Xcode보다 낫다고요? Gradle 지옥에 빠져보면 그 말이 쏙 들어갈 겁니다. minSdkVersion 충돌, Multidex 에러, Namespace 변경(Gradle 8.0), JDK 버전 문제, 그리고 의존성 트리 분석까지 완벽하게 해결해 봅니다.

서버를 끄지 않고 배포하는 법. 롤링, 카나리, 블루-그린의 차이점과 실제 구축 전략. DB 마이그레이션의 난제(팽창-수축 패턴)와 AWS CodeDeploy 활용법까지 심층 분석합니다.

새벽엔 낭비하고 점심엔 터지는 서버 문제 해결기. '택시 배차'와 '피자 배달' 비유로 알아보는 오토 스케일링과 서버리스의 차이, 그리고 Spot Instance를 활용한 비용 절감 꿀팁.

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과는 다르기 때문입니다.
가장 큰 오해는 "프론트엔드 코드에서 process.env를 쓸 수 있다"는 믿음입니다.
process 객체는 Node.js 런타임에만 존재하는 전역 객체입니다.process라는 변수가 아예 없습니다.CRA나 Webpack 기반 프로젝트에서는 빌드 도구가 소스 코드를 읽다가 process.env.REACT_APP_XXX를 발견하면, 빌드 시점에 그 자리에 실제 문자열 값을 때려 박아줍니다(Replacement).
즉, 브라우저가 process를 이해하는 게 아니라, 빌드 결과물에는 process가 사라지고 "실제값"만 남는 마법을 부린 것입니다.
Vite도 비슷한 마법을 부리지만, 주문(Syntax)이 다릅니다.
Vite는 최신 ES Modules 표준에 맞춰 import.meta.env라는 문법을 사용합니다.
Vite에서 환경 변수를 노출하려면 두 가지 규칙을 지켜야 합니다.
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
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}`);
TypeScript를 쓴다면 import.meta.env.VITE_...를 칠 때 자동 완성이 안 돼서 불편할 수 있습니다.
이를 해결하려면 타입 정의 파일(d.ts)을 확장해야 합니다.
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;
}
tsconfig.json 확인compilerOptions.types 배열에 "vite/client"가 포함되어 있는지 확인합니다.
{
"compilerOptions": {
"types": ["vite/client"]
}
}
이제 VS Code에서 import.meta.env.를 입력하면 정의한 변수들이 자동 완성 목록에 뜹니다!
실제로는 로컬(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 주소가 주입된 빌드 결과물이 나옵니다.
여기서 많은 개발자가 좌절하는 포인트가 있습니다. "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 파일 안에 텍스트로 박제되었기 때문입니다.
이 문제를 해결하려면 "window 객체 주입" 패턴을 써야 합니다.
public/config.js 생성
window.ENV = {
API_URL: "DEFAULT_URL_FOR_DEV"
};
index.html에서 로드
<head>
<script src="/config.js"></script>
</head>
// 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 원칙 준수).
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)
}
};
});
이렇게 하면 루트 레벨의 공통 환경 변수를 모든 앱이 공유할 수 있습니다.
VITE_를 꼭 붙이세요. (VITE_API_URL)process.env는 잊고 import.meta.env를 쓰세요..env 파일 내용은 서버가 뜰 때 로드됩니다. 파일 수정 후엔 반드시 재시작 (npm run dev 다시 실행) 하세요.vite-env.d.ts에 타입을 추가하면 개발이 편해집니다.window.ENV 주입 패턴을 고려하세요.Vite의 방식은 처음엔 낯설지만, 보안과 명시성 측면에서 훨씬 더 안전하고 현대적인 접근 방식입니다.