back

Posts

프론트엔드 개발자가 착각하기 쉬운 .env의 진실

2025-11-14

"브라우저에서 환경변수를 볼 수 있나요?"

답: 네, 볼 수 있습니다. 그것도 아주 쉽게요.

많은 프론트엔드 개발자들이 착각하는 부분이 있습니다

".env 파일을 배포하지 않으니까 환경변수는 안전하다"

하지만 이 생각은 실무에선 무척 위험합니다. 저도 처음 개발을 배울 때, 많은 강의에서 .env에 시크릿 키처럼 보안이 필요한 값들을 넣는 게 익숙했죠. 하지만 강의는 정말 학습을 위해 그렇게 노출할 뿐(물론 강사님들도 이 내용을 얘기하실 겁니다) 실무에선 다음을 명시해야 합니다.

환경변수 값이 빌드된 JavaScript 파일에 문자열로 박혀서 배포됩니다

// 코드에서 이렇게 작성하면
const apiUrl = process.env.REACT_APP_API_URL;

// 빌드 후 실제 JS 파일에는 이렇게 변환됩니다
const apiUrl = "https://api.example.com";

브라우저 개발자 도구로 확인하는 방법

  1. Sources 탭에서 확인

    • Chrome DevTools → Sources → 번들 파일 열기
    • Ctrl+F로 환경변수 값 검색
    • 모든 환경변수 값이 평문으로 보입니다
  2. 네트워크 탭에서 확인

    • JavaScript 파일 다운로드
    • 파일 내용 확인 → 환경변수 값 노출
  3. 소스맵이 있다면

    • 원본 코드와 환경변수 사용 위치까지 모두 확인 가능

이처럼 클라이언트에서 사용하는 모든 환경변수는 공개 정보입니다.

  • 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 같은 산출물에 들어갑니다

접두사 규칙

도구접두사사용 방법
CRAREACT_APP_*process.env.REACT_APP_*
ViteVITE_*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...