blog
운영 API가 10초씩 지연된 이유: 빌드 시점에 고정된 NEXT_PUBLIC_ 변수
Front-end / Next.js#Next.js#환경 변수#NEXT_PUBLIC#Docker#axios
증상
운영 배포 후 서버 컴포넌트의 API 요청이 10초 가까이 걸린 뒤 실패하거나 timeout이 났다. 개발 환경에서는 재현되지 않았다.
배경: 런타임 설정 주입 구조
이미지는 한 번 빌드하고, 컨테이너를 띄울 때 public/config.json을 마운트해서 환경별 API 주소를 주입하는 구조였다.
# 빌드 시 .env
NEXT_PUBLIC_API_URL=http://dev-server:19100
# 운영 컨테이너에 마운트
public/config.json → { "NEXT_PUBLIC_API_URL": "https://prod-server:8443" }
원인
// server-axios.ts (수정 전)
const baseURL =
process.env.NEXT_PUBLIC_API_URL ?? // 빌드 시 문자열로 치환됨
envManager.getData('NEXT_PUBLIC_API_URL') ?? // 런타임 config.json 값
'';
const instance = axios.create({ baseURL, timeout: 10000 });
NEXT_PUBLIC_* 변수는 next build 때 코드 안의 process.env.NEXT_PUBLIC_API_URL이 문자열로 치환된다. 클라이언트 번들뿐 아니라 서버 코드에서도 마찬가지다. 그래서 위 코드는 빌드 결과물에서 사실상 이렇게 된다.
const baseURL = 'http://dev-server:19100' ?? envManager.getData(...) ?? '';
??는 왼쪽이 null·undefined일 때만 오른쪽을 쓰는데, 왼쪽이 항상 문자열이므로 런타임 값은 한 번도 쓰이지 않았다. 운영 서버는 접근할 수 없는 개발 서버로 연결을 시도하다 10초 timeout에 걸렸다.
해결: 서버에서 config.json 직접 읽기
서버 코드는 파일 시스템에 접근할 수 있으므로, 컨테이너에 마운트된 config.json을 직접 읽어 우선 사용했다.
// utils/server-config.ts
import 'server-only';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
export const readServerConfig = (): EnvConfig => {
try {
const raw = readFileSync(join(process.cwd(), 'public', 'config.json'), 'utf-8');
const parsed = JSON.parse(raw) as Partial<EnvConfig>;
return {
NEXT_PUBLIC_API_URL: parsed.NEXT_PUBLIC_API_URL ?? process.env.NEXT_PUBLIC_API_URL ?? '',
};
} catch {
// config.json이 없으면 빌드 시 값으로 대체 (로컬 개발)
return { NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL ?? '' };
}
};
// server-axios.ts (수정 후)
const { NEXT_PUBLIC_API_URL: baseURL } = readServerConfig();
const serverInstance = axios.create({ baseURL, timeout: 10000 });
readServerConfig()를 모듈 최상단에서 호출하므로 파일은 서버 프로세스가 이 모듈을 처음 불러올 때 한 번 읽힌다. 요청마다 호출하는 위치로 옮기면 매번 디스크를 읽게 되니 위치에 주의한다.- standalone 빌드라면
process.cwd()가 standalone 서버 디렉터리이므로, 그 아래public/에config.json이 마운트되어 있어야 한다.
더 단순한 대안: 서버 전용 환경 변수
서버에서만 쓰는 값이라면 NEXT_PUBLIC_ 접두사를 붙이지 않는 것이 가장 간단하다. 접두사가 없는 변수는 빌드 시 치환되지 않고 런타임에 process.env에서 읽힌다.
// 서버 코드
const baseURL = process.env.API_URL; // 컨테이너 실행 시 주입한 값
docker run -e API_URL=https://prod-server:8443 my-app
클라이언트에서도 같은 값이 필요하다면 지금처럼 config.json을 fetch하거나, 서버 컴포넌트에서 읽어 props로 내려준다.
정리
| 항목 | 수정 전 | 수정 후 |
|---|---|---|
| 서버 baseURL 출처 | 빌드 시 치환된 NEXT_PUBLIC_API_URL | 런타임에 읽은 config.json |
| 운영 연결 대상 | 개발 서버 | 운영 서버 |
| 응답 | 약 10초 후 timeout | 정상 |
교훈
NEXT_PUBLIC_*는 빌드 시점 상수다. 런타임에 바뀌어야 하는 값은 이 접두사를 쓰지 않거나, 별도 런타임 설정을 읽는다.- 빌드 시점 상수를
??체인 맨 앞에 두면 뒤의 런타임 값은 절대 쓰이지 않는다. 우선순위가 높은 런타임 값을 앞에 둔다. - "개발에서는 되는데 운영에서만 느리다"면, 운영 환경이 엉뚱한 주소로 연결을 시도하고 있는지 먼저 의심한다.