프론트엔드 개발자가 착각하기 쉬운 .env의 진실
2025-11-14"브라우저에서 환경변수를 볼 수 있나요?"
답: 네, 볼 수 있습니다. 그것도 아주 쉽게요.
많은 프론트엔드 개발자들이 착각하는 부분이 있습니다
".env 파일을 배포하지 않으니까 환경변수는 안전하다"
하지만 이 생각은 실무에선 무척 위험합니다. 저도 처음 개발을 배울 때, 많은 강의에서 .env에 시크릿 키처럼 보안이 필요한 값들을 넣는 게 익숙했죠. 하지만 강의는 정말 학습을 위해 그렇게 노출할 뿐(물론 강사님들도 이 내용을 얘기하실 겁니다) 실무에선 다음을 명시해야 합니다.
환경변수 값이 빌드된 JavaScript 파일에 문자열로 박혀서 배포됩니다
// 코드에서 이렇게 작성하면
const apiUrl = process.env.REACT_APP_API_URL;
// 빌드 후 실제 JS 파일에는 이렇게 변환됩니다
const apiUrl = "https://api.example.com";
브라우저 개발자 도구로 확인하는 방법
-
Sources 탭에서 확인
- Chrome DevTools → Sources → 번들 파일 열기
Ctrl+F로 환경변수 값 검색- 모든 환경변수 값이 평문으로 보입니다
-
네트워크 탭에서 확인
- JavaScript 파일 다운로드
- 파일 내용 확인 → 환경변수 값 노출
-
소스맵이 있다면
- 원본 코드와 환경변수 사용 위치까지 모두 확인 가능
이처럼 클라이언트에서 사용하는 모든 환경변수는 공개 정보입니다.
- API 키, 비밀 토큰 등은 절대 클라이언트 환경변수에 넣으면 안 됩니다
.env파일 자체를 배포하지 않아도, 값은 이미 번들에 포함되어 있습니다- 결국 브라우저가 실행해야 하므로 "난독화"나 "암호화"로는 막을 수 없습니다
왜 .env 값이 "공개"되는가: 빌드 타임 치환 메커니즘
프론트엔드 번들러(webpack, esbuild 등)는 코드 내의 환경변수 참조를 빌드 시점에 상수로 치환합니다.
빌드 전 코드
// CRA
const apiUrl = process.env.REACT_APP_API_URL;
// Vite
const apiUrl = import.meta.env.VITE_API_URL;
// Next.js (클라이언트)
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
빌드 후 결과물
// 실제 번들된 JS 파일
const apiUrl = "https://api.example.com/v1";
치환 후에는 실제 문자열이 결과 JS/HTML에 박혀 배포됩니다. 따라서 브라우저, 소스 보기, 소스맵을 통해 값이 노출됩니다.
1) React(SPA)
빌드/배포에서의 포함 방식
React의 .env 파일 그 자체는 업로드되지 않습니다. 대신 .env 값은 빌드 시점에 번들에 인라인되어 main.*.js, index.html 같은 산출물에 들어갑니다
접두사 규칙
| 도구 | 접두사 | 사용 방법 |
|---|---|---|
| CRA | REACT_APP_* | process.env.REACT_APP_* |
| Vite | VITE_* | import.meta.env.VITE_* |
*접두사가 없는 환경변수는 빌드 과정에서 무시되고 번들에 포함되지 않습니다.
2) Next.js
Next.js는 환경변수를 두 가지 타입으로 구분합니다:
| 타입 | 접두사 | 노출 범위 | 사용 위치 |
|---|---|---|---|
| 클라이언트 노출 | NEXT_PUBLIC_* | 브라우저에 노출 | 클라이언트 컴포넌트, 서버 컴포넌트 |
| 서버 전용 | 없음 | 서버에서만 접근 | 서버 컴포넌트, API Routes, getServerSideProps |
코드 예시
API Route (서버 전용)
// pages/api/user.ts 또는 app/api/user/route.ts
export async function GET() {
// 서버에서만 실행 - 비밀키 사용 가능(브라우저 노출 위험 없음)
const dbPassword = process.env.DB_PASSWORD; // 접두사 없음
const apiSecret = process.env.EXTERNAL_API_SECRET;
// 데이터 처리 후 결과만 반환
const data = await fetchUserData(apiSecret, dbPassword);
return Response.json(data);
}
클라이언트 컴포넌트
// components/Map.tsx
"use client";
export default function Map() {
// NEXT_PUBLIC_*는 클라이언트에서 사용 가능
const mapKey = process.env.NEXT_PUBLIC_MAP_KEY;
// 접두사 없는 변수는 클라이언트에서 undefined
const secret = process.env.SECRET_KEY; // undefined!
return <div>Map with key: {mapKey}</div>;
}
서버 컴포넌트 (App Router)
// app/dashboard/page.tsx
export default async function Dashboard() {
// 서버 컴포넌트에서는 모든 환경변수 접근 가능
const dbUrl = process.env.DATABASE_URL;
const publicUrl = process.env.NEXT_PUBLIC_API_URL;
const data = await fetchData(dbUrl);
return <div>{/* UI */}</div>;
}
그럼 CRA에서 .env에 뭘 저장해야 하나?
저장해도 되는 것들
# 1. API 엔드포인트 (공개 정보)
REACT_APP_API_URL=https://api.myapp.com
# 2. 공개 ID (추적용, 분석용)
REACT_APP_GA_ID=G-XXXXXXXXXX
REACT_APP_SENTRY_DSN=https://xxx@sentry.io/xxx
# 3. 기능 플래그 (공개되어도 무방)
REACT_APP_FEATURE_BETA=true
# 4. 공개 메타 정보
REACT_APP_APP_VERSION=1.0.0
REACT_APP_BUILD_ENV=production
# 5. 도메인 제한이 걸린 공개 키
REACT_APP_GOOGLE_MAPS_KEY=AIza... (도메인 제한 설정 필수)
REACT_APP_FIREBASE_API_KEY=AIza...