Vite에서 mode에 맞는 env 사용하기

GwangSoo·2026년 1월 7일

개인공부

목록 보기
42/43
post-thumbnail

Vite를 이용해서 개발을 할 때 mode에 맞는 env를 사용할 수 있다고 공식 문서에 나와있다.

.env                # 모든 상황에서 사용될 환경 변수
.env.local          # 모든 상황에서 사용되나, 로컬 개발 환경에서만 사용될(Git에 의해 무시될) 환경 변수
.env.[mode]         # 특정 모드에서만 사용될 환경 변수
.env.[mode].local   # 특정 모드에서만 사용되나, 로컬 개발 환경에서만 사용될(Git에 의해 무시될) 환경 변수

이번 글에서는 Vite가 mode에 따라서 그에 맞는 환경변수 파일을 가져오는 방식을 소스코드를 기준으로 알아보겠다.

mode 적용 방법

mode를 따로 적용하기 위해서는 개발 서버 시작 시 --mode 옵션을 넘겨줘야 한다.

vite --mode staging

실제로 mode 값을 다르게 줬을 때 각각 다른 파일을 불러오는 것을 확인할 수 있다.

mode 미지정 (기본 mode: development)

# .env
VITE_CURRENT_FILE_NAME=.env

development

pnpm dev(= pnpm vite)로 실행할 시 mode의 기본 값은 development가 된다.

mode: staging

# .env.staging
VITE_CURRENT_FILE_NAME=.env.staging

staging

pnpm dev --mode staging(= pnpm vite --mode staging)으로 실행 시 mode가 staging이 되는 것을 알 수 있다.

동작 방식

그렇다면 내부적으로 어떻게 동작을 하기에 mode 옵션에 의해 불러오는 환경변수 파일이 달라지는 걸까?

최초 서버 실행

CLI를 통해 Vite를 실행시키면 아래처럼 서버를 생성하는 코드가 실행된다.

const { createServer } = await import('./server')
try {
  const server = await createServer({
    root,
    base: options.base,
    mode: options.mode,
    configFile: options.config,
    configLoader: options.configLoader,
    logLevel: options.logLevel,
    clearScreen: options.clearScreen,
    server: cleanGlobalCLIOptions(options),
    forceOptimizeDeps: options.force,
    experimental: {
      bundledDev: options.experimentalBundle,
    },
  })

https://github.com/vitejs/vite/blob/10b24952cf0121410c45537931b609de60ae0471/packages/vite/src/node/cli.ts#L216C30-L216C42

이때 커맨드라인 옵션으로 받은 mode를 그대로 넘겨주는 것을 알 수 있다.

createServer

export function createServer(
  inlineConfig: InlineConfig | ResolvedConfig = {},
): Promise<ViteDevServer> {
  return _createServer(inlineConfig, { listen: true })
}

export async function _createServer(
  inlineConfig: ResolvedConfig | InlineConfig | undefined = {},
  options: {
    listen: boolean
    previousEnvironments?: Record<string, DevEnvironment>
    previousShortcutsState?: ShortcutsState<ViteDevServer>
  },
): Promise<ViteDevServer> {
  const config = isResolvedConfig(inlineConfig)
    ? inlineConfig
    : await resolveConfig(inlineConfig, 'serve')
    
  ...

https://github.com/vitejs/vite/blob/10b24952cf0121410c45537931b609de60ae0471/packages/vite/src/node/server/index.ts#L431

서버를 생성하는 과정에서 아직 resolve되지 않은 config에 대해 resolveConfig를 호출하는 구조다.

즉, 최초 서버 실행 시에는 항상 config를 resolve하는 과정을 거친다.

resolveConfig

const userEnv = loadEnv(mode, envDir, resolveEnvPrefix(config))

https://github.com/vitejs/vite/blob/10b24952cf0121410c45537931b609de60ae0471/packages/vite/src/node/config.ts#L1316

config를 resolve하는 과정 중에 env 파일을 불러오는 코드가 실행된다.

이때 전달되는 mode 값은 앞에서 CLI 옵션으로 넘긴 값이다.

loadEnv

export function getEnvFilesForMode(
  mode: string,
  envDir: string | false,
): string[] {
  if (envDir !== false) {
    return [
      /** default file */ `.env`,
      /** local file */ `.env.local`,
      /** mode file */ `.env.${mode}`,
      /** mode local file */ `.env.${mode}.local`,
    ].map((file) => normalizePath(path.join(envDir, file)))
  }

  return []
}

export function loadEnv(
  mode: string,
  envDir: string | false,
  prefixes: string | string[] = 'VITE_',
): Record<string, string> {
  ...
  
  const env: Record<string, string> = {}
  const envFiles = getEnvFilesForMode(mode, envDir)

  const parsed = Object.fromEntries(
    envFiles.flatMap((filePath) => {
      if (!tryStatSync(filePath)?.isFile()) return []

      return Object.entries(parse(fs.readFileSync(filePath)))
    }),
  )
  
  ...

https://github.com/vitejs/vite/blob/10b24952cf0121410c45537931b609de60ae0471/packages/vite/src/node/env.ts#L26

위 로직을 하나하나 보겠다.

  1. env 파일 경로 세팅

mode가 staging일 경우, getEnvFilesForMode의 결과물은 아래와 같다.

[
  '/some/path/.env',
  '/some/path/.env.local',
  '/some/path/.env.staging',
  '/some/path/.env.staging.local',
]

이 단계에서는 어떤 env 파일을 어떤 순서로 읽을지만 결정된다.

아직 값이 병합되거나 필터링되지는 않는다.

  1. env 파일들을 순서대로 읽어 entries로 변환

위에서 만든 파일 경로 목록을 기준으로 존재하는 파일만 읽어서 [key, value] 형태의 entries 배열을 만든다. 이때 같은 key가 여러 파일에 존재하면, 앞의 파일에서 나온 entry 뒤에, 뒤의 파일 entry가 계속 쌓이게 된다.

  1. Object.fromEntries를 통해 뒤에 온 값으로 덮어씌운다
const obj: Record<string, string> = {}
for (const [key, value] of entries) {
  obj[key] = value
}

Object.fromEntries는 entries를 앞에서부터 순서대로 객체에 할당한다. 따라서 같은 key가 여러 번 등장하면, 가장 마지막에 등장한 값이 최종 값이 된다.

이 구조 때문에 .env.staging.local.env, .env.local, .env.staging의 값을 자연스럽게 덮어쓰게 된다.

마무리하며

업무를 하다가 환경변수가 적용되지 않아 습관적으로 .env.development로 수정했을 때 정상적으로 동작하는 것을 보고 그냥 넘어갔었다. 그런데 같이 일하시는 분께서 왜 동작했는지 알아보면 좋을 것 같다고 말씀해주셔서 살펴보게 되었고, 결과적으로 유익한 시간이 되었다.

참고

0개의 댓글