SPA를 한 번 빌드해 여러 환경에 띄우기 (런타임 설정)
API 주소처럼 환경마다 다른 값을, 빌드에 박지 않고 앱이 켜질 때 읽게 만들어 한 번 빌드로 여러 환경에 배포하는 방법을 정리합니다.
결론부터 말하면, 환경별 값은 빌드에 박지 말고, 앱이 켜질 때 읽는 런타임 설정 파일로 분리합니다. 그러면 앱은 한 번만 빌드하고, 설정 파일만 환경에 맞게 갈아 끼우면 됩니다.
빌드타임 주입과 런타임 주입, 무엇이 다른가
차이는 환경별 값을 언제 결정하느냐입니다. 빌드타임 주입은 빌드할 때 값을 코드에 박아 넣고, 런타임 주입은 앱이 브라우저에서 켜질 때 값을 읽습니다.
| 구분 | 빌드타임 주입 | 런타임 주입 |
|---|---|---|
| 값이 정해지는 시점 | 빌드할 때 | 앱이 켜질 때 |
| 환경이 N개면 | N번 빌드 | 한 번 빌드 + 설정 N개 |
| 대표 예 | import.meta.env.VITE_* | window.__ENV__ |
빌드타임 주입(VITE_*, REACT_APP_* 등)은 편하지만, 값이 결과물에 박혀 버립니다. 환경이 늘면 그만큼 다시 빌드해야 합니다.
왜 런타임 설정인가
런타임 설정이 필요한 이유는, 같은 앱을 환경마다 다른 값으로 띄워야 할 때 매번 다시 빌드할 수 없기 때문입니다. 특히 고객사마다 API 주소가 다른 온프레미스 배포에서는 빌드타임 주입이 곧 무너집니다.
한 번 인쇄한 안내 책자에 비유할 수 있습니다. 본문(앱)은 한 번만 찍고, 마지막 주소 페이지(설정)만 환경마다 끼워 넣 는 것입니다. 책자를 통째로 다시 찍지 않아도 됩니다.
구현 방법
핵심은 빌드 결과물 바깥에 설정 파일을 따로 두고, 앱이 시작할 때 그 파일을 읽게 하는 것입니다.

1. 설정 파일을 따로 둔다
빌드에 포함하지 않는 env-config.js를 만들어 전역 객체에 값을 싣습니다.
// env-config.js (빌드 산출물이 아니라 배포 시 갈아 끼우는 파일)
window.__ENV__ = {
API_BASE_URL: "https://api.example.com",
};
2. index.html에서 먼저 읽는다
앱 번들보다 먼저 설정 파일을 로드합니다.
<script src="/env-config.js"></script>
<script type="module" src="/assets/index.js"></script>
3. 코드에서 사용한다
const API_BASE_URL =
window.__ENV__?.API_BASE_URL ?? "http://localhost:8080"; // 로컬 기본값
4. 환경마다 설정 파일 만 교체
배포 환경에서 env-config.js만 그 환경 값으로 바꿉니다. 쿠버네티스라면 ConfigMap을 볼륨으로 마운트해 이 파일을 덮어쓰는 식입니다. 앱 번들은 그대로입니다.
흔한 함정 (CDN 캐시)
가장 자주 데이는 함정은 CDN 캐시입니다. 화면 파일은 빠른 응답을 위해 CDN에 캐시되는데, 설정 파일까지 캐시되면 값을 바꿔도 옛 설정이 계속 내려갑니다.
해결은 설정 파일만 캐시를 끄는 것입니다.
Cache-Control: no-store # env-config.js 에만 적용

그 외 주의점:
- 빌드타임과 런타임을 섞지 않기:
import.meta.env와window.__ENV__를 한 값에 섞으면 어느 쪽이 적용됐는지 헷갈립니다. 환경별 값은 런타임으로 일원화합니다. - 로컬 기본값 두기: 설정 파일이 없을 때를 대비해 코드에 안전한 기본값(
?? localhost)을 둡니다. - 민감 정보 금지:
env-config.js는 브라우저가 그대로 읽습니다. 비밀 키 같은 민감 값은 절대 넣지 않습니다.
Q&A
- Next.js 같은 SSR도 같은 방법인가요?
- SSR은 서버에서 환경 변수를 직접 읽을 수 있어 사정이 다릅니다. 이 방법은 정적으로 빌드되는 SPA(CSR)에서 특히 유용합니다.
.env파일을 여러 개 두면 안 되나요?.env.production등은 결국 빌드타임에 박힙니다. 환경이 빌드 후에 정해지는 경우(온프레미스 등)에는 런타임 설정이 필요합니다.
- 설정 값이 많아지면요?
window.__ENV__객체에 모읍니다. 타입 안전을 위해 한 곳에서 읽어 검증한 뒤 앱 전역에 제공하는 래퍼를 두면 좋습니다.
참고자료
- MDN:
Cache-Control - 12-Factor App: Config