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정상

교훈

  1. NEXT_PUBLIC_*는 빌드 시점 상수다. 런타임에 바뀌어야 하는 값은 이 접두사를 쓰지 않거나, 별도 런타임 설정을 읽는다.
  2. 빌드 시점 상수를 ?? 체인 맨 앞에 두면 뒤의 런타임 값은 절대 쓰이지 않는다. 우선순위가 높은 런타임 값을 앞에 둔다.
  3. "개발에서는 되는데 운영에서만 느리다"면, 운영 환경이 엉뚱한 주소로 연결을 시도하고 있는지 먼저 의심한다.