홈 [환경 변수] .env 파일 완벽 이해하기: 종류, 우선순위, 보안 그리고 Next.js·Vite 활용법
포스트
취소

[환경 변수] .env 파일 완벽 이해하기: 종류, 우선순위, 보안 그리고 Next.js·Vite 활용법

들어가며: .env는 비밀번호를 숨겨 주는 파일일까?

프론트엔드 프로젝트를 실행하다 보면 루트 디렉터리에서 다음과 같은 파일을 자주 만나게 됩니다.

.env
.env.local
.env.development
.env.production
.env.test

그리고 코드에서는 다음과 같이 값을 읽습니다.

1
const apiUrl = process.env.API_URL;

또는 Vite 프로젝트라면 다음처럼 사용합니다.

1
const apiUrl = import.meta.env.VITE_API_URL;

처음 접하면 이런 의문이 생깁니다.

.env 파일은 왜 필요한가?
.env.development와 .env.production은 언제 읽히는가?
.env.local에서 local은 localhost라는 뜻인가?
같은 변수가 여러 파일에 있으면 어느 값이 선택되는가?
API 키를 .env에 넣으면 브라우저에서도 안전한가?

이 글에서는 파일 이름만 외우지 않고, 환경 변수와 빌드 과정의 관계부터 이해해 보겠습니다.


먼저 환경 변수란?

환경 변수(environment variable)는 프로그램 외부에서 프로그램에 전달하는 설정값입니다.

예를 들어 같은 애플리케이션이라도 실행되는 환경에 따라 API 주소가 달라질 수 있습니다.

개발 환경 API
https://dev-api.example.com

운영 환경 API
https://api.example.com

주소를 코드에 직접 작성하면 환경이 바뀔 때마다 코드를 수정해야 합니다.

1
2
// 환경에 따라 소스 코드를 직접 바꿔야 한다.
const apiUrl = "https://dev-api.example.com";

환경 변수로 분리하면 애플리케이션 코드는 그대로 두고 외부 설정만 바꿀 수 있습니다.

1
const apiUrl = process.env.API_URL;
코드
const apiUrl = process.env.API_URL

개발 환경
API_URL=https://dev-api.example.com

운영 환경
API_URL=https://api.example.com

환경 변수는 보통 다음 값을 관리할 때 사용합니다.

  • 환경별 API 주소
  • 데이터베이스 연결 주소
  • 외부 서비스 키
  • 기능 활성화 여부
  • 로그 레벨
  • 배포 환경 이름
  • 서버 포트

핵심은 코드와 환경별 설정을 분리하는 것입니다.


운영체제의 환경 변수와 .env 파일은 다르다

환경 변수는 원래 운영체제나 프로세스가 프로그램에 전달합니다.

PowerShell에서는 다음처럼 현재 프로세스에 환경 변수를 설정할 수 있습니다.

1
2
$env:API_URL = "https://api.example.com"
node server.js

macOS나 Linux 셸에서는 다음과 같이 실행할 수 있습니다.

1
API_URL=https://api.example.com node server.js

Node.js에서는 process.env를 통해 이 값을 읽습니다.

1
console.log(process.env.API_URL);

하지만 개발할 때마다 여러 변수를 터미널에 입력하는 것은 번거롭습니다.

그래서 환경 변수 목록을 파일에 적어 두고, 실행 도구가 읽어 프로세스에 넣어 주는 방식이 널리 사용됩니다. 그 파일이 .env입니다.

API_URL=https://api.example.com
LOG_LEVEL=debug

관계를 정리하면 다음과 같습니다.

.env 파일
   ↓ 프레임워크나 dotenv 로더가 읽음
환경 변수로 로드
   ↓
process.env 또는 import.meta.env로 접근

따라서 .env 자체가 특별한 JavaScript 문법인 것은 아닙니다.

어떤 .env 파일을 읽을지, 어떤 순서로 합칠지, 브라우저 코드에 어떤 값을 넣을지는 사용하는 런타임과 프레임워크가 결정합니다.


순수 Node.js에서 .env 불러오기

최근 Node.js에서는 --env-file 옵션으로 파일을 직접 불러올 수 있습니다.

# .env
API_URL=https://api.example.com
PORT=3000
1
node --env-file=.env server.js
1
2
3
// server.js
console.log(process.env.API_URL);
console.log(process.env.PORT);

다른 이름의 파일도 명시할 수 있습니다.

1
node --env-file=.env.development server.js

여기서 중요한 점은 Node.js가 파일 이름의 development를 해석해 실행 환경을 자동으로 판단하는 것이 아니라는 점입니다.

node --env-file=.env.development server.js
                     ↑
개발자가 이 파일을 읽으라고 직접 지정

Node.js 버전이나 기존 프로젝트 구성에 따라 dotenv 패키지를 사용하는 경우도 있습니다.

1
npm install dotenv
1
2
3
import "dotenv/config";

console.log(process.env.API_URL);

프레임워크 프로젝트에서는 Next.js나 Vite가 이미 환경 변수 로딩 기능을 제공하므로, 이유 없이 dotenv.config()를 중복 호출할 필요는 없습니다.


.env 기본 문법

가장 기본적인 형식은 KEY=VALUE입니다.

API_URL=https://api.example.com
APP_NAME=My App
PORT=3000
FEATURE_CHAT=true

주석

# 뒤는 주석으로 처리됩니다.

# 백엔드 API 주소
API_URL=https://api.example.com

값 자체에 #이 필요하면 따옴표로 감싸는 편이 안전합니다.

PASSWORD="abc#123"

따옴표

공백이나 특수문자가 있는 값은 따옴표로 감쌀 수 있습니다.

APP_NAME="My Application"
MESSAGE='hello world'

단순한 URL이나 숫자는 따옴표 없이 작성해도 됩니다.

API_URL=https://api.example.com
PORT=3000

모든 값은 문자열이다

이 부분을 자주 놓칩니다.

PORT=3000
FEATURE_CHAT=false

Node.js에서 읽으면 값은 숫자와 불리언이 아니라 문자열입니다.

1
2
console.log(typeof process.env.PORT); // "string"
console.log(process.env.FEATURE_CHAT); // "false"

따라서 다음 코드는 의도대로 동작하지 않습니다.

1
2
3
4
if (process.env.FEATURE_CHAT) {
  // 문자열 "false"도 빈 문자열이 아니므로 truthy다.
  enableChat();
}

명시적으로 변환해야 합니다.

1
2
const port = Number(process.env.PORT ?? 3000);
const isChatEnabled = process.env.FEATURE_CHAT === "true";

.env 파일 종류 이해하기

파일 이름의 의미부터 하나씩 살펴보겠습니다.

.env

모든 환경에서 공통으로 사용할 기본값을 작성합니다.

APP_NAME=My Service
API_TIMEOUT=5000

보통 다른 환경별 파일이 같은 변수를 덮어쓸 수 있는 가장 기본 계층입니다.

다만 .env를 실제로 Git에 포함할지는 프로젝트 정책과 프레임워크에 따라 결정해야 합니다. 비밀값이 들어 있다면 커밋하지 않는 것이 원칙입니다.

.env.development

개발 모드에서 사용할 값을 작성합니다.

API_URL=http://localhost:8080
LOG_LEVEL=debug

Vite의 기본 개발 서버나 Next.js의 next dev처럼 개발 모드로 실행할 때 선택됩니다.

.env.production

운영 빌드 또는 운영 모드에서 사용할 값을 작성합니다.

API_URL=https://api.example.com
LOG_LEVEL=error

파일 이름에 production이 들어 있다고 자동으로 안전해지는 것은 아닙니다. 이 파일도 평범한 텍스트 파일이므로 비밀값을 저장한 채 저장소에 올리면 안 됩니다.

.env.test

테스트 실행 시 사용할 값을 작성합니다.

DATABASE_URL=postgresql://localhost:5432/app_test
API_URL=http://localhost:4000

테스트가 실제 개발 데이터나 운영 데이터에 접근하지 않도록 별도 값을 두는 것이 중요합니다.

.env.local

현재 개발자의 컴퓨터에서만 사용할 개인 설정을 작성합니다.

API_URL=http://192.168.0.10:8080
PERSONAL_DEBUG_TOKEN=example

여기서 local은 반드시 localhost를 뜻하지 않습니다.

local = 이 컴퓨터 또는 이 작업 공간에서만 사용하는 덮어쓰기 값

팀원마다 값이 다를 수 있으므로 일반적으로 Git에 커밋하지 않습니다.

.env.development.local

개발 모드이면서 현재 컴퓨터에서만 사용할 값을 작성합니다.

API_URL=http://localhost:9090

.env.development의 팀 공통 개발 설정을 개인 컴퓨터에서만 덮어쓸 때 유용합니다.

.env.production.local

운영 모드용 개인 덮어쓰기 파일입니다.

로컬에서 운영 빌드를 재현하거나 특정 배포 환경을 점검할 때 사용할 수 있습니다. 실제 배포 비밀값은 파일보다 배포 플랫폼의 환경 변수 관리 기능을 사용하는 편이 안전합니다.

.env.test.local

테스트 모드에서 현재 컴퓨터에만 적용할 값을 작성합니다.

테스트 러너나 프레임워크에 따라 .env.local을 테스트에서 일부러 무시하기도 하므로 정확한 로딩 규칙을 확인해야 합니다.

.env.example

필요한 변수 이름과 예시만 보여 주는 문서 역할의 파일입니다.

DATABASE_URL=
API_URL=http://localhost:8080
SESSION_SECRET=

실제 비밀값은 넣지 않고 Git에 커밋합니다.

새 팀원은 이 파일을 보고 자신의 .env.local을 만들 수 있습니다.

.env.example       Git에 포함, 실제 비밀값 없음
.env.local         Git에서 제외, 개인의 실제 값

중요한 점은 .env.example이 대부분의 프레임워크에서 자동으로 로드되는 파일은 아니라는 것입니다. 사람을 위한 설정 명세에 가깝습니다.


파일 이름은 약속이고 로딩 규칙은 도구가 정한다

.env.development라는 이름을 만들었다고 모든 프로그램이 자동으로 읽는 것은 아닙니다.

Node.js 자체
Next.js
Vite
Create React App
Docker Compose
테스트 러너

각 도구는 지원하는 파일 이름과 우선순위가 다를 수 있습니다.

따라서 다음처럼 단정하면 안 됩니다.

.env.local은 언제나 .env.production보다 우선한다.

정확한 표현은 다음과 같습니다.

내가 사용하는 프레임워크의 로딩 순서에서
.env.local이 어느 위치에 있는지 확인해야 한다.

이제 프론트엔드에서 많이 사용하는 Next.js와 Vite를 비교해 보겠습니다.


Next.js의 .env 로딩 순서

Next.js는 환경 변수를 다음 순서로 찾고, 값을 발견하면 그 값을 사용합니다.

높은 우선순위

1. 이미 process.env에 존재하는 값
2. .env.$(NODE_ENV).local
3. .env.local
4. .env.$(NODE_ENV)
5. .env

낮은 우선순위

NODE_ENV가 development라면 다음 순서가 됩니다.

process.env
.env.development.local
.env.local
.env.development
.env

예를 들어 세 파일에 같은 변수가 있다고 해보겠습니다.

# .env
API_URL=https://default.example.com
# .env.development
API_URL=https://dev.example.com
# .env.local
API_URL=http://localhost:8080

next dev에서는 .env.local의 값이 선택됩니다.

http://localhost:8080

Next.js는 테스트 환경에서 테스트 결과의 재현성을 높이기 위해 .env.local을 로드하지 않습니다.

또한 /src 디렉터리를 사용하는 프로젝트라도 .env 파일은 일반적으로 프로젝트 루트에 둡니다.

my-app/
├─ .env.local
├─ package.json
├─ next.config.ts
└─ src/
   └─ app/

Next.js에서 환경 변수 사용하기

서버 코드에서는 process.env로 접근할 수 있습니다.

1
2
3
4
5
export async function getUser() {
  const response = await fetch(`${process.env.INTERNAL_API_URL}/user`);

  return response.json();
}

서버에서만 사용하는 값에는 NEXT_PUBLIC_을 붙이지 않습니다.

DATABASE_URL=postgresql://example
SESSION_SECRET=very-secret-value
INTERNAL_API_URL=https://internal.example.com

이 값은 Server Component, Route Handler, Server Action과 같은 서버 영역에서만 읽어야 합니다.

브라우저에 전달할 값

클라이언트 코드에서 읽어야 하는 변수에는 NEXT_PUBLIC_ 접두사가 필요합니다.

NEXT_PUBLIC_API_URL=https://api.example.com
NEXT_PUBLIC_APP_NAME=My App
1
2
3
export function AppName() {
  return <strong>{process.env.NEXT_PUBLIC_APP_NAME}</strong>;
}

NEXT_PUBLIC_ 변수는 빌드 과정에서 브라우저용 JavaScript에 포함될 수 있습니다.

따라서 이름에 PUBLIC이 붙은 값은 비밀이 아닙니다.

# 절대 이렇게 하지 않는다.
NEXT_PUBLIC_DATABASE_PASSWORD=secret
NEXT_PUBLIC_PRIVATE_API_KEY=secret

브라우저 개발자 도구나 빌드 결과에서 확인할 수 있다고 가정해야 합니다.


Vite의 .env 로딩 순서

Vite는 다음 파일을 사용합니다.

.env
.env.local
.env.[mode]
.env.[mode].local

개발 모드라면 다음 파일들을 함께 읽습니다.

.env
.env.local
.env.development
.env.development.local

우선순위를 단순화하면 다음과 같습니다.

높은 우선순위

1. Vite 실행 전에 이미 존재하는 환경 변수
2. .env.[mode].local
3. .env.[mode]
4. .env.local
5. .env

낮은 우선순위

모드 전용 파일이 공통 파일보다 우선합니다.

# .env
VITE_API_URL=https://default.example.com
# .env.development
VITE_API_URL=https://dev.example.com

npm run dev로 실행하면 .env.development의 값이 사용됩니다.

Vite는 시작할 때 .env 파일을 읽으므로 파일을 변경했다면 개발 서버를 다시 시작해야 합니다.


Vite에서 환경 변수 사용하기

Vite 애플리케이션 코드에서는 process.env가 아니라 import.meta.env를 사용합니다.

VITE_API_URL=https://api.example.com
1
const apiUrl = import.meta.env.VITE_API_URL;

기본 설정에서는 VITE_ 접두사가 붙은 변수만 클라이언트 코드에 노출됩니다.

VITE_API_URL=https://api.example.com
DATABASE_PASSWORD=secret
1
2
console.log(import.meta.env.VITE_API_URL); // 사용 가능
console.log(import.meta.env.DATABASE_PASSWORD); // undefined

하지만 VITE_가 붙었다는 것은 암호화되었다는 뜻이 아닙니다.

VITE_ 접두사
→ 브라우저 코드에서 사용할 수 있도록 공개한다는 뜻
→ 비밀값을 넣으면 안 된다

Vite는 자주 사용하는 몇 가지 값을 기본으로 제공합니다.

1
2
3
4
import.meta.env.MODE; // 현재 mode
import.meta.env.DEV; // 개발 환경 여부
import.meta.env.PROD; // 운영 환경 여부
import.meta.env.SSR; // 서버 사이드 렌더링 여부

Vite의 mode와 NODE_ENV는 같은 개념이 아니다

Vite에서는 mode와 NODE_ENV를 구분해야 합니다.

기본 명령은 다음 mode를 사용합니다.

vite
→ development mode

vite build
→ production mode

원한다면 사용자 정의 mode를 만들 수 있습니다.

1
2
3
4
5
6
7
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "build:staging": "vite build --mode staging"
  }
}

그리고 .env.staging을 만듭니다.

VITE_API_URL=https://staging-api.example.com
VITE_ENV_NAME=staging
1
npm run build:staging

이때 선택되는 mode는 staging입니다.

1
console.log(import.meta.env.MODE); // "staging"

하지만 vite build --mode staging도 여전히 프로덕션 빌드를 수행합니다. mode는 어떤 .env.[mode] 파일을 읽을지 정하는 이름이고, NODE_ENV는 라이브러리들이 개발용인지 운영용인지 판단할 때 사용하는 별도 개념입니다.

mode
→ 어떤 설정 묶음을 선택할지 나타내는 Vite 개념

NODE_ENV
→ 코드와 라이브러리가 개발·운영 동작을 구분하는 환경 변수

둘을 무조건 같은 값이라고 가정하지 않는 것이 좋습니다.


Next.js와 Vite 비교

항목Next.jsVite
서버 환경 변수 접근process.env.NAMENode 영역에서는 도구 설정에 따라 다름
브라우저 환경 변수 접근process.env.NEXT_PUBLIC_NAMEimport.meta.env.VITE_NAME
공개 접두사NEXT_PUBLIC_VITE_
개발 환경 파일.env.development.env.development
운영 환경 파일.env.production.env.production
사용자 정의 모드일반적으로 NODE_ENV 임의 변경을 권장하지 않음vite --mode staging 지원
파일 변경 후개발 서버 재시작 권장개발 서버 재시작 필요

같은 .env 파일을 사용하더라도 코드에서 읽는 방법과 우선순위는 다릅니다.


.env에 넣으면 비밀값이 안전해질까?

아닙니다.

.env는 암호화 파일이 아니라 평범한 텍스트 파일입니다.

.env에 저장했다
≠ 암호화되었다
≠ 브라우저에서 숨겨진다
≠ Git에 올려도 안전하다

안전 여부는 값이 어디에서 사용되는지에 따라 결정됩니다.

서버에서만 읽는 값

DATABASE_URL=postgresql://example
SESSION_SECRET=secret
PAYMENT_PRIVATE_KEY=secret

서버 프로세스에서만 읽고 브라우저 번들에 포함하지 않는다면 클라이언트에 직접 노출되지 않습니다.

브라우저에 포함되는 값

NEXT_PUBLIC_API_URL=https://api.example.com
VITE_GOOGLE_MAP_ID=example

브라우저가 기능을 수행하려면 값을 내려받아야 하므로 사용자가 볼 수 있습니다.

따라서 공개 변수에는 노출되어도 괜찮은 값만 넣어야 합니다.

공개되어도 되는 값
API의 공개 주소, 공개 프로젝트 ID, UI 설정

공개되면 안 되는 값
DB 비밀번호, 서버 전용 토큰, private key, service role key

클라이언트에서 외부 API를 호출해야 하지만 비밀키가 필요한 경우에는 서버 API를 중간에 둡니다.

브라우저
  ↓ 비밀키 없이 요청
내 서버의 Route Handler 또는 API 서버
  ↓ 서버에서 비밀키 사용
외부 API

.gitignore는 어떻게 작성할까?

프로젝트 정책에 따라 다르지만 다음과 같은 형태를 자주 사용합니다.

# 실제 환경 변수 파일 제외
.env
.env.*

# 변수 이름을 설명하는 예시 파일은 포함
!.env.example

또는 공통값이 들어 있는 .env를 저장소에서 관리하고, 개인 파일만 제외한다면 다음처럼 구성할 수도 있습니다.

.env.local
.env.*.local

단, 추적하는 .env에는 비밀값을 넣지 않아야 합니다.

이미 Git에 커밋했다면

나중에 .gitignore에 추가해도 이미 추적 중인 파일은 자동으로 사라지지 않습니다.

1
git rm --cached .env

더 중요한 점은 파일 삭제만으로 유출된 비밀이 안전해지지 않는다는 것입니다.

1. 노출된 키와 비밀번호 폐기 또는 재발급
2. 저장소에서 추적 중인 파일 제거
3. .gitignore 추가
4. 배포 환경의 값도 교체
5. 필요하면 Git 기록 정리 검토

비밀값이 한 번 커밋되었다면 이미 노출되었다고 가정하고 반드시 교체해야 합니다.


빌드 시점 환경 변수와 런타임 환경 변수

프론트엔드 환경 변수를 이해할 때 가장 중요한 개념 중 하나입니다.

빌드 시점 값

Vite의 import.meta.env.VITE_*와 Next.js의 NEXT_PUBLIC_* 값은 보통 빌드할 때 브라우저 코드에 삽입됩니다.

빌드 실행
VITE_API_URL=https://api-a.example.com
   ↓
JavaScript 번들에 값 포함
   ↓
배포 후 환경 변수만 api-b로 변경
   ↓
이미 만들어진 번들은 api-a를 계속 사용

값을 바꾸려면 다시 빌드해야 할 수 있습니다.

런타임 값

서버가 요청을 처리하는 시점에 process.env에서 읽는 값은 배포 환경에서 주입된 최신 값을 사용할 수 있습니다.

1
2
3
4
5
export async function GET() {
  const region = process.env.DEPLOY_REGION;

  return Response.json({ region });
}

다만 프레임워크의 정적 생성, 캐시, 번들 최적화에 따라 읽는 시점이 달라질 수 있습니다. 반드시 다음을 구분해야 합니다.

이 코드는 브라우저에서 실행되는가?
서버에서 실행되는가?
빌드할 때 한 번 실행되는가?
요청마다 실행되는가?

실무에서 추천하는 파일 구성

예를 들어 Vite 프로젝트라면 다음처럼 구성할 수 있습니다.

my-app/
├─ .env.example
├─ .env.development
├─ .env.production
├─ .env.local              # Git 제외
├─ .gitignore
└─ package.json
# .env.example
VITE_API_URL=
VITE_SENTRY_DSN=
# .env.development
VITE_API_URL=https://dev-api.example.com
# .env.production
VITE_API_URL=https://api.example.com
# .env.local
VITE_API_URL=http://localhost:8080

Next.js 프로젝트에서는 공개값과 서버 전용 값을 명확히 구분합니다.

# .env.example
DATABASE_URL=
SESSION_SECRET=
NEXT_PUBLIC_API_URL=
1
2
3
4
5
// 서버 코드에서만 사용
const databaseUrl = process.env.DATABASE_URL;

// 브라우저에 공개되어도 되는 값
const publicApiUrl = process.env.NEXT_PUBLIC_API_URL;

운영 비밀값은 저장소의 .env.production에 넣기보다 Vercel, GitHub Actions, AWS 같은 배포 시스템의 환경 변수 또는 Secret 관리 기능에 저장하는 편이 좋습니다.


환경 변수도 애플리케이션 입력값이다

환경 변수가 존재한다고 해서 올바른 값이라는 보장은 없습니다.

1
const apiUrl = process.env.API_URL;

값이 없으면 undefined이고, 잘못된 URL이나 숫자가 들어올 수도 있습니다.

작은 프로젝트에서는 시작할 때 직접 검사할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function requireEnv(name: string): string {
  const value = process.env[name];

  if (!value) {
    throw new Error(`Missing environment variable: ${name}`);
  }

  return value;
}

export const env = {
  databaseUrl: requireEnv("DATABASE_URL"),
  sessionSecret: requireEnv("SESSION_SECRET"),
};

여러 값과 타입을 관리한다면 Zod 같은 스키마 검증 도구를 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
import { z } from "zod";

const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  SESSION_SECRET: z.string().min(32),
  PORT: z.coerce.number().int().positive().default(3000),
  ENABLE_ANALYTICS: z.enum(["true", "false"]).transform((value) => {
    return value === "true";
  }),
});

export const env = envSchema.parse(process.env);

이렇게 하면 애플리케이션이 한참 실행된 뒤 이상하게 실패하는 대신 시작 단계에서 설정 오류를 발견할 수 있습니다.

환경 변수 검증 없음
→ 기능을 사용한 시점에 뒤늦게 오류

환경 변수 검증 있음
→ 애플리케이션 시작 시 누락과 형식 오류 발견

TypeScript에서 Vite 환경 변수 타입 지정하기

Vite에서는 사용자 정의 변수의 타입을 선언할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
// src/vite-env.d.ts
interface ImportMetaEnv {
  readonly VITE_API_URL: string;
  readonly VITE_APP_NAME: string;
  readonly VITE_ENABLE_MOCK: "true" | "false";
}

interface ImportMeta {
  readonly env: ImportMetaEnv;
}

그러면 오타를 더 빨리 발견할 수 있습니다.

1
2
import.meta.env.VITE_API_URL;
import.meta.env.VITE_APL_URL; // 선언되지 않은 이름

하지만 TypeScript 선언은 실제 실행 환경의 값을 검증하지 않습니다.

TypeScript 타입 선언
→ 개발 중 자동 완성과 정적 검사

런타임 스키마 검증
→ 실제 배포 환경에 들어온 값 검사

둘의 역할은 다릅니다.


자주 발생하는 실수

1. .env를 수정했는데 값이 바뀌지 않는다

대부분의 도구는 시작할 때 .env를 읽습니다.

개발 서버를 종료하고 다시 실행합니다.

1
npm run dev

2. Vite에서 process.env를 사용한다

Vite의 클라이언트 코드는 기본적으로 import.meta.env를 사용합니다.

1
2
3
4
5
// 잘못된 예
process.env.VITE_API_URL;

// 올바른 예
import.meta.env.VITE_API_URL;

3. Vite 변수에 VITE_ 접두사가 없다

# 클라이언트에 노출되지 않음
API_URL=https://api.example.com

# 클라이언트에서 사용 가능
VITE_API_URL=https://api.example.com

4. Next.js 클라이언트 변수에 NEXT_PUBLIC_이 없다

Client Component에서 사용할 값이라면 공개 접두사가 필요합니다.

NEXT_PUBLIC_API_URL=https://api.example.com

단, 접두사를 붙이는 순간 공개값으로 취급해야 합니다.

5. false를 불리언으로 생각한다

FEATURE_ENABLED=false

실제로는 문자열이므로 명시적으로 비교합니다.

1
const isEnabled = process.env.FEATURE_ENABLED === "true";

6. .env.local의 local을 localhost로 이해한다

.env.local은 현재 컴퓨터 전용 덮어쓰기 파일이라는 의미입니다. 값은 원격 API 주소일 수도 있습니다.

7. 운영 서버가 내 .env.production을 자동으로 읽는다고 생각한다

배포 플랫폼이 어떤 방식으로 환경 변수를 주입하는지 확인해야 합니다. 많은 배포 환경에서는 대시보드나 Secret 저장소에 등록한 값을 빌드 또는 런타임에 주입합니다.

8. 공개 접두사가 붙은 변수에 비밀키를 넣는다

NEXT_PUBLIC_과 VITE_는 보안 기능이 아니라 브라우저 노출 허용 표시입니다.

9. 빌드 후 환경 변수만 바꾸면 프론트 값도 바뀐다고 생각한다

브라우저 번들에 삽입된 값은 다시 빌드해야 변경되는 경우가 많습니다.

10. 같은 이름을 여러 파일에 넣고 우선순위를 확인하지 않는다

다음 명령이나 임시 로그로 실제 선택된 mode와 값을 확인하되, 비밀값 전체를 로그에 출력하지 않습니다.

1
2
3
4
console.log({
  mode: import.meta.env.MODE,
  hasApiUrl: Boolean(import.meta.env.VITE_API_URL),
});

상황별로 어떤 파일을 사용해야 할까?

모든 환경에서 같은 공개 기본값

.env

팀이 공유하는 개발 환경 설정

.env.development

팀이 공유하는 운영용 비밀이 아닌 설정

.env.production

내 컴퓨터에서만 값을 덮어쓰기

.env.local

개발 환경에서만 개인 설정 적용

.env.development.local

테스트 전용 설정

.env.test

필요한 변수 목록을 팀원에게 안내

.env.example

실제 운영 비밀값

배포 플랫폼의 Environment Variables 또는 Secret Manager

전체 흐름으로 이해하기

환경 변수가 사용되는 과정을 한 번에 정리하면 다음과 같습니다.

1. 실행 명령이 mode 또는 NODE_ENV를 결정한다.

2. 프레임워크가 규칙에 따라 .env 파일들을 읽는다.

3. 같은 이름의 변수가 있다면 우선순위에 따라 하나를 선택한다.

4. 서버 변수는 process.env 등에서 읽는다.

5. 공개 접두사가 붙은 일부 변수는 브라우저 번들에 포함된다.

6. 애플리케이션은 문자열 값을 필요한 타입으로 변환하고 검증한다.

예를 들어 Vite 개발 환경에서 다음 파일이 있다고 해보겠습니다.

# .env
VITE_API_URL=https://default.example.com
VITE_APP_NAME=My App
# .env.development
VITE_API_URL=https://dev.example.com
# .env.development.local
VITE_API_URL=http://localhost:8080

npm run dev를 실행하면 최종 결과는 다음과 같습니다.

1
2
3
4
5
import.meta.env.VITE_API_URL;
// "http://localhost:8080"

import.meta.env.VITE_APP_NAME;
// "My App"

높은 우선순위의 파일은 같은 변수만 덮어쓰고, 정의하지 않은 변수는 낮은 우선순위 파일의 값을 그대로 사용할 수 있습니다.


마무리

.env를 단순히 API 키를 적어 두는 파일로만 이해하면 파일이 늘어날수록 혼란스러워집니다.

핵심은 다음과 같습니다.

.env는 환경 변수를 작성하는 텍스트 파일이다.

파일을 읽고 합치는 규칙은 프레임워크가 결정한다.

.env.local의 local은 localhost가 아니라 개인 환경을 뜻한다.

모든 값은 문자열이므로 변환과 검증이 필요하다.

NEXT_PUBLIC_과 VITE_가 붙은 값은 브라우저에 공개된다.

.env는 암호화 파일이 아니며 비밀값을 Git에 올리면 안 된다.

프론트엔드 공개 변수는 대개 빌드 시점에 번들에 포함된다.

새로운 .env.* 파일을 만났을 때 파일 이름만 보고 추측하지 말고 다음 세 가지를 확인해 보세요.

어떤 명령과 mode에서 읽히는가?
여러 파일 중 우선순위는 무엇인가?
그 값이 서버에만 남는가, 브라우저에 포함되는가?

이 기준을 잡으면 Next.js, Vite뿐 아니라 다른 도구에서도 환경 변수 설정을 훨씬 안전하게 다룰 수 있습니다.


참고 자료

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

[SVG] 선이 그려지는 애니메이션 완벽 이해하기: 원리부터 React 활용까지

-