본문으로 건너뛰기

환경 변수 사용 방법

TanStack Router 애플리케이션에서 API 엔드포인트, 기능 플래그 및 여러 번들러의 빌드 구성을 위해 환경 변수를 구성하고 사용하는 방법을 알아봅니다.

빠른 시작

TanStack Router의 환경 변수는 주로 클라이언트 측 구성에 사용되며, 보안을 위해 번들러별 명명 규칙을 따라야 합니다.

# .env
VITE_API_URL=https://api.example.com
VITE_ENABLE_DEVTOOLS=true
// Route configuration
import { createFileRoute } from '@tanstack/react-router'

export const Route = createFileRoute('/posts')({
loader: async () => {
const apiUrl = import.meta.env.VITE_API_URL
const response = await fetch(`${apiUrl}/posts`)
return response.json()
},
component: PostsList,
})

환경 변수 액세스 패턴

Vite 기반 프로젝트(가장 일반적)

Vite에서는 클라이언트 코드에서 환경 변수에 액세스할 수 있도록 VITE_ 접두사를 붙여야 합니다.

// Route loaders
export const Route = createFileRoute('/dashboard')({
loader: async () => {
const apiUrl = import.meta.env.VITE_API_URL // ✅ Works
const apiKey = import.meta.env.VITE_PUBLIC_API_KEY // ✅ Works

// This would be undefined (security feature):
// const secret = import.meta.env.SECRET_KEY // ❌ Undefined

return fetchDashboardData(apiUrl, apiKey)
},
})

// Components
export function ApiStatus() {
const isDev = import.meta.env.DEV // ✅ Built-in Vite variable
const isProd = import.meta.env.PROD // ✅ Built-in Vite variable
const mode = import.meta.env.MODE // ✅ development/production

return (
<div>
Environment: {mode}
{isDev && <DevToolsPanel />}
</div>
)
}

Webpack 기반 프로젝트

환경 변수를 주입하도록 webpack의 DefinePlugin을 구성합니다. 참고: Webpack은 기본적으로 import.meta.env를 지원하지 않으므로 process.env 패턴을 사용합니다.

// webpack.config.js
const webpack = require('webpack')

module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.API_URL': JSON.stringify(process.env.API_URL),
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.ENABLE_FEATURE': JSON.stringify(process.env.ENABLE_FEATURE),
}),
],
}

// Usage in routes
export const Route = createFileRoute('/api-data')({
loader: async () => {
const response = await fetch(`${process.env.API_URL}/data`)
return response.json()
},
component: () => {
const enableFeature = process.env.ENABLE_FEATURE === 'true'
return enableFeature ? <NewFeature /> : <OldFeature />
},
})

Rspack 기반 프로젝트

Rspack은 PUBLIC_ 접두사 규칙을 사용합니다. 참고: import.meta.env 지원은 Rspack 구성과 런타임에 따라 다르므로 builtins.define을 올바르게 구성해야 할 수 있습니다.

# .env
PUBLIC_API_URL=https://api.example.com
PUBLIC_FEATURE_FLAG=true
// Route usage
export const Route = createFileRoute('/features')({
loader: async () => {
const apiUrl = import.meta.env.PUBLIC_API_URL
return fetch(`${apiUrl}/features`).then(r => r.json())
},
component: () => {
const enableFeature = import.meta.env.PUBLIC_FEATURE_FLAG === 'true'
return enableFeature ? <NewFeature /> : <OldFeature />
},
})

ESBuild 프로젝트

define을 수동으로 구성합니다.

// build script
import { build } from 'esbuild'

await build({
entryPoints: ['src/main.tsx'],
define: {
'process.env.NODE_ENV': '"production"',
'process.env.API_URL': `"${process.env.API_URL}"`,
},
})

일반적인 패턴

라우트 로더의 API 구성

// src/routes/posts/index.tsx
import { createFileRoute } from '@tanstack/react-router'

const fetchPosts = async () => {
const baseUrl = import.meta.env.VITE_API_URL
const apiKey = import.meta.env.VITE_API_KEY

const response = await fetch(`${baseUrl}/posts`, {
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
})

if (!response.ok) {
throw new Error('Failed to fetch posts')
}

return response.json()
}

export const Route = createFileRoute('/posts/')({
loader: fetchPosts,
errorComponent: ({ error }) => (
<div>Error loading posts: {error.message}</div>
),
})

환경 기반 라우트 구성

// src/routes/__root.tsx
import { createRootRoute, Outlet } from '@tanstack/react-router'
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'

export const Route = createRootRoute({
component: () => (
<>
<Outlet />
{/* Only show devtools in development */}
{import.meta.env.DEV && <TanStackRouterDevtools />}
</>
),
})

라우트의 기능 플래그

// src/lib/features.ts
export const features = {
enableNewDashboard: import.meta.env.VITE_ENABLE_NEW_DASHBOARD === 'true',
enableAnalytics: import.meta.env.VITE_ENABLE_ANALYTICS === 'true',
debugMode: import.meta.env.DEV,
}

// src/routes/dashboard/index.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'
import { features } from '../../lib/features'

export const Route = createFileRoute('/dashboard/')({
beforeLoad: () => {
// Redirect to old dashboard if new one is disabled
if (!features.enableNewDashboard) {
throw redirect({ to: '/dashboard/legacy' })
}
},
component: NewDashboard,
})

인증 구성

// src/lib/auth.ts
export const authConfig = {
domain: import.meta.env.VITE_AUTH0_DOMAIN,
clientId: import.meta.env.VITE_AUTH0_CLIENT_ID,
redirectUri: `${window.location.origin}/callback`,
}

// src/routes/_authenticated.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'
import { authConfig } from '../lib/auth'

export const Route = createFileRoute('/_authenticated')({
beforeLoad: async ({ location }) => {
const isAuthenticated = await checkAuthStatus()

if (!isAuthenticated) {
// Redirect to auth provider
const authUrl = `https://${authConfig.domain}/authorize?client_id=${authConfig.clientId}&redirect_uri=${authConfig.redirectUri}`
window.location.href = authUrl
return
}
},
})

환경 구성과 검색 매개변수

// src/routes/search.tsx
import { createFileRoute } from '@tanstack/react-router'
import { z } from 'zod'

const searchSchema = z.object({
q: z.string().optional(),
category: z.string().optional(),
})

export const Route = createFileRoute('/search')({
validateSearch: searchSchema,
loader: async ({ search }) => {
const apiUrl = import.meta.env.VITE_SEARCH_API_URL
const params = new URLSearchParams({
q: search.q || '',
category: search.category || 'all',
api_key: import.meta.env.VITE_SEARCH_API_KEY,
})

const response = await fetch(`${apiUrl}/search?${params}`)
return response.json()
},
})

환경 파일 설정

파일 계층(Vite)

Vite는 다음 순서로 환경 파일을 로드합니다.

.env.local          # Local overrides (add to .gitignore)
.env.production # Production-specific
.env.development # Development-specific
.env # Default (commit to git)

구성 예제

.env(저장소에 커밋):

# API Configuration
VITE_API_URL=https://api.example.com
VITE_API_VERSION=v1

# Feature Flags
VITE_ENABLE_NEW_UI=false
VITE_ENABLE_ANALYTICS=true

# Auth Configuration (public keys only)
VITE_AUTH0_DOMAIN=your-domain.auth0.com
VITE_AUTH0_CLIENT_ID=your-client-id

# Build Configuration
VITE_APP_NAME=TanStack Router App
VITE_APP_VERSION=1.0.0

.env.local(.gitignore에 추가):

# Development overrides
VITE_API_URL=http://localhost:3001
VITE_ENABLE_NEW_UI=true
VITE_DEBUG_MODE=true

.env.production:

# Production-specific
VITE_API_URL=https://api.prod.example.com
VITE_ENABLE_ANALYTICS=true
VITE_ENABLE_NEW_UI=true

타입 안전성

Vite TypeScript 선언

src/vite-env.d.ts를 생성합니다.

/// <reference types="vite/client" />

interface ImportMetaEnv {
// API Configuration
readonly VITE_API_URL: string
readonly VITE_API_VERSION: string
readonly VITE_API_KEY?: string

// Feature Flags
readonly VITE_ENABLE_NEW_UI: string
readonly VITE_ENABLE_ANALYTICS: string
readonly VITE_DEBUG_MODE?: string

// Authentication
readonly VITE_AUTH0_DOMAIN: string
readonly VITE_AUTH0_CLIENT_ID: string

// App Configuration
readonly VITE_APP_NAME: string
readonly VITE_APP_VERSION: string
}

interface ImportMeta {
readonly env: ImportMetaEnv
}

런타임 검증

Zod를 사용해 시작 시 대체값 및 선택적 값과 함께 환경 변수를 검증합니다.

// src/config/env.ts
import { z } from 'zod'

const envSchema = z.object({
// Required variables
VITE_API_URL: z.string().url(),
VITE_AUTH0_DOMAIN: z.string(),
VITE_AUTH0_CLIENT_ID: z.string(),
VITE_APP_NAME: z.string(),

// Optional with defaults
VITE_API_VERSION: z.string().default('v1'),
VITE_ENABLE_NEW_UI: z.string().default('false'),
VITE_ENABLE_ANALYTICS: z.string().default('true'),

// Optional variables
VITE_DEBUG_MODE: z.string().optional(),
VITE_SENTRY_DSN: z.string().optional(),
})

// Validate at app startup with fallbacks
export const env = envSchema.parse({
...import.meta.env,
// Provide fallbacks for missing optional values
VITE_API_VERSION: import.meta.env.VITE_API_VERSION || 'v1',
VITE_ENABLE_NEW_UI: import.meta.env.VITE_ENABLE_NEW_UI || 'false',
VITE_ENABLE_ANALYTICS: import.meta.env.VITE_ENABLE_ANALYTICS || 'true',
})

// Typed helper functions
export const isFeatureEnabled = (flag: keyof typeof env) => {
return env[flag] === 'true'
}

// Type-safe boolean conversion
export const getBooleanEnv = (
value: string | undefined,
defaultValue = false,
): boolean => {
if (value === undefined) return defaultValue
return value === 'true'
}

타입 안전성을 사용한 사용

// src/routes/api-data.tsx
import { createFileRoute } from '@tanstack/react-router'
import { env, isFeatureEnabled } from '../config/env'

export const Route = createFileRoute('/api-data')({
loader: async () => {
// TypeScript knows these are strings and exist
const response = await fetch(`${env.VITE_API_URL}/${env.VITE_API_VERSION}/data`)
return response.json()
},
component: () => {
return (
<div>
<h1>{env.VITE_APP_NAME}</h1>
{isFeatureEnabled('VITE_ENABLE_NEW_UI') && <NewUIComponent />}
</div>
)
},
})

번들러별 구성

Vite 구성

// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { tanstackRouter } from '@tanstack/router-plugin/vite'

export default defineConfig({
plugins: [
react(),
// tanstackRouter generates route tree and enables file-based routing
tanstackRouter(),
],
// Environment variables are handled automatically
// Custom environment variable handling:
define: {
// Global constants (these become available as global variables)
__APP_VERSION__: JSON.stringify(process.env.npm_package_version),
},
})

Webpack 구성

// webpack.config.js
const { TanStackRouterWebpack } = require('@tanstack/router-webpack-plugin')
const webpack = require('webpack')

module.exports = {
plugins: [
// TanStackRouterWebpack generates route tree and enables file-based routing
new TanStackRouterWebpack(),
new webpack.DefinePlugin({
// Inject environment variables (use process.env for Webpack)
'process.env.API_URL': JSON.stringify(process.env.API_URL),
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.ENABLE_FEATURE': JSON.stringify(process.env.ENABLE_FEATURE),
}),
],
}

Rspack 구성

// rspack.config.js
const { TanStackRouterRspack } = require('@tanstack/router-rspack-plugin')

module.exports = {
plugins: [
// TanStackRouterRspack generates route tree and enables file-based routing
new TanStackRouterRspack(),
],
// Rspack automatically handles PUBLIC_ prefixed variables for import.meta.env
// Custom handling for additional variables:
builtins: {
define: {
// Define additional variables (these become global replacements)
'process.env.API_URL': JSON.stringify(process.env.PUBLIC_API_URL),
__BUILD_TIME__: JSON.stringify(new Date().toISOString()),
},
},
}

프로덕션 체크리스트

  • 클라이언트에 노출되는 모든 변수가 적절한 접두사(VITE_, PUBLIC_ 등)를 사용합니다.
  • 환경 변수에 민감한 데이터(API 시크릿, 개인 키)가 없습니다.
  • .env.local.gitignore에 있습니다.
  • 프로덕션 환경 변수가 호스팅 플랫폼에 구성되어 있습니다.
  • 필수 환경 변수가 빌드 시 검증됩니다.
  • TypeScript 선언이 최신 상태입니다.
  • 기능 플래그가 프로덕션에 맞게 구성되어 있습니다.
  • API URL이 프로덕션 엔드포인트를 가리킵니다.

일반적인 문제

환경 변수가 정의되지 않음

문제: import.meta.env.MY_VARIABLEundefined를 반환합니다.

해결 방법:

  1. 올바른 접두사 추가: Vite에는 VITE_, Rspack에는 PUBLIC_을 사용합니다. Vite의 기본 접두사는 구성에서 변경할 수 있습니다.
    // vite.config.ts
    export const config = {
    // ...rest of your config
    envPrefix: 'MYPREFIX_', // this means `MYPREFIX_MY_VARIABLE` is the new correct way
    }
  2. 새 변수를 추가한 후 개발 서버를 다시 시작합니다.
  3. 파일 위치 확인: .env 파일은 프로젝트 루트에 있어야 합니다.
  4. 번들러 구성 확인: 변수가 올바르게 주입되는지 확인합니다.
  5. 변수 확인:
  • 개발 시: 올바른 .env 파일 또는 환경에 있는지 확인합니다.
  • 프로덕션: 올바른 .env 파일 또는 번들 시점의 현재 환경에 있는지 확인합니다. VITE_/PUBLIC_ 접두사가 붙은 변수는 번들 시 매크로와 같은 방식으로 대체되며 서버에서 런타임에 절대 읽히지 않습니다. 이는 흔한 실수이므로 해당하지 않는지 확인합니다.

예제:

# ❌ Won't work (no prefix)
API_KEY=abc123

# ✅ Works with Vite
VITE_API_KEY=abc123

# ✅ Works with Rspack
PUBLIC_API_KEY=abc123

# ❌ Won't bundle the variable (assuming it is not set in the environment of the build)
npm run build

# ✅ Works with Vite and will bundle the variable for production
VITE_API_KEY=abc123 npm run build

# ✅ Works with Rspack and will bundle the variable for production
PUBLIC_API_KEY=abc123 npm run build

프로덕션 런타임의 런타임 클라이언트 환경 변수

문제: VITE_/PUBLIC_ 변수가 번들 시점에만 대체된다면 런타임 변수를 클라이언트에서 사용할 수 있게 하려면 어떻게 해야 하나요?

해결 방법:

서버에서 클라이언트로 변수를 전달합니다.

  1. 올바른 env. 파일에 변수를 추가합니다.
  2. 클라이언트에서 값을 읽을 수 있도록 서버에 엔드포인트를 생성합니다.

예제:

선호하는 백엔드 프레임워크 또는 라이브러리를 사용해도 되지만, 여기서는 Tanstack Start 서버 함수를 사용합니다.

const getRuntimeVar = createServerFn({ method: 'GET' }).handler(() => {
return process.env.MY_RUNTIME_VAR // notice `process.env` on the server, and no `VITE_`/`PUBLIC_` prefix
})

export const Route = createFileRoute('/')({
loader: async () => {
const foo = await getRuntimeVar()
return { foo }
},
component: RouteComponent,
})

function RouteComponent() {
const { foo } = Route.useLoaderData()
// ... use your variable however you want
}

변수가 업데이트되지 않음

문제: 환경 변수 변경 사항이 앱에 반영되지 않습니다.

해결 방법:

  1. 개발 서버 다시 시작 - 새 변수에 필요합니다.
  2. 파일 계층 확인 - .env.local.env를 재정의합니다.
  3. 브라우저 캐시 지우기 - 강력 새로고침(Ctrl+Shift+R)을 수행합니다.
  4. 올바른 파일 확인 - 올바른 .env 파일을 편집하고 있는지 확인합니다.

TypeScript 오류

문제: Property 'VITE_MY_VAR' does not exist on type 'ImportMetaEnv'

해결 방법: src/vite-env.d.ts에 선언을 추가합니다.

interface ImportMetaEnv {
readonly VITE_MY_VAR: string
}

빌드 오류

문제: 빌드 중 환경 변수가 없습니다.

해결 방법:

  1. CI/CD 구성: 빌드 환경에 변수를 설정합니다.
  2. 검증 추가: 빌드 시 필수 변수를 확인합니다.
  3. .env 파일 사용: 프로덕션 .env 파일이 있는지 확인합니다.
  4. 번들러 구성 확인: 환경 변수 주입을 확인합니다.

보안 문제

문제: 민감한 데이터가 실수로 노출됩니다.

해결 방법:

  1. 클라이언트 변수에 시크릿을 절대 사용하지 않습니다. - 브라우저에 표시됩니다.
  2. 민감한 API 호출에는 서버 측 프록시를 사용합니다.
  3. 번들 감사 - 빌드된 파일에서 유출된 시크릿을 확인합니다.
  4. 명명 규칙 준수 - 접두사가 있는 변수만 노출됩니다.

런타임과 빌드 시점의 혼동

문제: 런타임에 변수를 사용할 수 없습니다.

해결 방법:

  1. 정적 대체 이해 - 변수는 빌드 시 대체됩니다.
  2. 동적 값에는 서버 측 사용 - 런타임 구성에는 API를 사용합니다.
  3. 시작 시 검증 - 필요한 모든 변수가 있는지 확인합니다.

환경 변수는 항상 문자열임

문제: 불리언 또는 숫자 값을 비교할 때 예상하지 못한 동작이 발생합니다.

해결 방법:

  1. 항상 문자열로 비교: === true가 아닌 === 'true'를 사용합니다.
  2. 명시적으로 변환: parseInt(), parseFloat() 또는 Boolean()을 사용합니다.
  3. 헬퍼 함수 사용: 타입이 지정된 변환 유틸리티를 생성합니다.

예제:

// ❌ Won't work as expected
const isEnabled = import.meta.env.VITE_FEATURE_ENABLED // This is a string!
if (isEnabled) {
/* Always true if variable exists */
}

// ✅ Correct string comparison
const isEnabled = import.meta.env.VITE_FEATURE_ENABLED === 'true'

// ✅ Safe numeric conversion
const port = parseInt(import.meta.env.VITE_PORT || '3000', 10)

// ✅ Helper function approach
const getBooleanEnv = (value: string | undefined, defaultValue = false) => {
if (value === undefined) return defaultValue
return value.toLowerCase() === 'true'
}

일반적인 다음 단계