모듈 최상단 환경 변수 검증이 NextJS 빌드를 깨뜨린 이유

환경 변수가 없으면 바로 예외를 던지는 코드를 모듈 최상단에 두었다가, 실제로는 아무 요청도 보내지 않는 프로덕션 빌드 단계에서 빌드가 깨진 경험을 정리한 글입니다. NextJS App Router 프로젝트에서 겪었지만, 모듈을 불러오는 순간 코드가 실행되는 JavaScript·TypeScript 프로젝트라면 어디서든 생길 수 있는 문제입니다.
상황
자체 호스팅한 에이전트 오케스트레이터에 정기 작업(루틴)을 등록하는 API 라우트를 만들고 있었습니다. 루틴마다 담당 에이전트가 다르고, 일부 역할은 값이 없으면 기본 담당 에이전트로 넘어가도록 되어 있었습니다. 처음 구현에서는 역할별 에이전트 ID 표를 모듈 최상단 상수로 만들었고, 그 표를 만드는 함수 안에서 필수 값이 없으면 바로 에러를 던졌습니다.
로컬 개발 서버에서는 env 파일에 값이 다 들어 있어서 문제가 보이지 않았습니다.
증상
빌드 환경에서 `pnpm build`를 돌리자 페이지 데이터를 모으는 단계에서 "필수 에이전트 ID 환경 변수 누락" 에러를 내며 빌드가 멈췄습니다. 실패한 위치는 루틴 등록 API 라우트였습니다. 그 라우트는 요청이 들어올 때만 외부 API를 부르는데, 빌드 중에는 요청이 들어오지 않습니다. 그런데도 검증이 실행된 것입니다.
처음에는 역할별 기본값 처리가 꼬인 줄 알고 기본값 처리를 공통 함수 하나로 합쳤습니다. 코드는 깔끔해졌지만 빌드는 여전히 실패했습니다. 기본값 처리가 아니라 검증이 실행되는 시점이 문제였기 때문입니다.
원인
`next build`는 라우트를 찾은 다음 서버 코드를 묶고, 각 라우트를 정적으로 만들지 요청 시점에 그릴지 판단합니다. 이 과정에서 라우트 모듈을 실제로 불러와 평가합니다. 모듈 최상단 코드는 불러오는 순간 한 번 실행되므로, 거기 있는 `process` 객체의 env 검증도 빌드 머신에서 돌아갑니다.
정리하면 이렇습니다.
- 런타임에만 넣는 비밀값·ID는 빌드 환경에 없는 경우가 많습니다. 특히 이미지 하나를 만들어 여러 환경에 배포하는 구성이 그렇습니다.
- 그런데 검증 코드는 "값을 쓰는 시점"이 아니라 "모듈을 불러오는 시점"에 묶여 있었습니다.
- 결국 실제로 외부 호출을 하지 않는 빌드 단계까지 필수 값을 요구하게 됐습니다.
`NEXT_PUBLIC_` 접두사가 붙은 값은 빌드 때 번들에 그대로 박히지만, 나머지 서버용 값은 런타임에 읽는 값이라는 점을 같이 생각하면 더 분명해집니다. 런타임 값을 빌드 시점에 강제하면 안 됩니다.
해결
검증을 외부 작업을 실제로 실행하는 경계로 옮겼습니다.
1. 모듈을 불러올 때는 표만 만듭니다. 역할별 값이 있으면 그 값을, 없으면 기본 역할 값을 그대로 넣고 비어 있어도 예외를 던지지 않습니다.
2. 루틴 생성 요청을 실제로 만들 때 ID를 확정하는 함수를 부르고, 여기서 값이 비어 있으면 명확한 메시지로 실패합니다.
```ts
// 바꾸기 전: import하는 순간 검증
export const AGENT_IDS = {
writer: requireEnv("WRITER_AGENT_ID"),
reviewer: requireEnv("REVIEWER_AGENT_ID"),
};
// 바꾼 뒤: import 시에는 값만 모으고, 쓰는 순간 검증
const { env } = process;
const AGENT_ID_ENV = {
writer: env["WRITER_AGENT_ID"] ?? env["DEFAULT_AGENT_ID"],
reviewer: env["REVIEWER_AGENT_ID"] ?? env["DEFAULT_AGENT_ID"],
};
function resolveAgentId(role: keyof typeof AGENT_ID_ENV): string {
const id = AGENT_ID_ENV[role];
if (!id) throw new Error(`${role} 담당 에이전트 ID가 설정되지 않았습니다`);
return id;
}
export async function createRoutine(role: keyof typeof AGENT_ID_ENV) {
const agentId = resolveAgentId(role);
// 외부 API 호출
}
```
이렇게 바꾼 뒤 빌드는 통과했고, 값이 빠진 상태로 배포하면 해당 API를 처음 호출할 때 원인이 분명한 에러가 납니다. 빨리 실패한다는 장점은 그대로 두고, 실패하는 위치만 맞는 곳으로 옮긴 셈입니다.
서버가 뜰 때 한 번에 확인하고 싶다면 NextJS의 instrumentation `register` 함수처럼 서버 시작 시점에 실행되는 훅에 검증을 두는 방법도 있습니다. 이 경우에도 빌드가 아니라 실행 환경에서 검사한다는 점은 같습니다.
다시 겪지 않으려면
- 모듈 최상단에서 필수 환경 변수 검증으로 예외를 던지지 않을 것
- 런타임 비밀값·ID는 외부 호출을 만드는 함수 안이나 서버 시작 훅에서 검증할 것
- 빌드 단계에서 실제로 필요한 값(번들에 들어가는 공개 값 등)과 런타임 값을 구분해 문서에 적어 둘 것
- CI 빌드는 런타임 비밀값을 넣지 않은 상태로 돌려 이런 의존을 일찍 잡을 것
- 빌드 에러에 라우트 경로가 찍히면 그 라우트가 불러오는 모듈의 최상단 코드부터 볼 것
참고 자료
- How to use environment variables in Next.js — Next.js, 2026-08-25
- Building your application — Next.js, 2026-11-06