RSS듀오랩스
Next.js

Next.js 환경 변수: NEXT_PUBLIC_ 인라인과 로드 순서

작성자
듀오랩스 대표·7분 읽기

같은 Docker 이미지를 스테이징과 운영에 함께 올리는 구성이 있습니다. 환경 변수는 각 환경의 설정으로 주입합니다. 서버 쪽 값은 환경에 맞게 잘 바뀌는데, 브라우저에서 쓰는 분석 도구 ID 만 두 환경에서 같은 값이 나옵니다. 스테이징의 방문 기록이 운영 통계에 섞입니다.

NEXT_PUBLIC_ 은 브라우저에서도 읽히게 해 주는 표시라는 생각

Next.js 에서 환경 변수를 브라우저에서 쓰려면 이름 앞에 NEXT_PUBLIC_ 을 붙입니다. 흔히 이것을 「이 값은 공개해도 된다는 표시」, 즉 접근 권한을 여는 스위치로 이해합니다. 서버에서 읽던 값을 클라이언트에서도 읽을 수 있게 해 주는 것이라고 보는 것입니다.

환경 변수 문서의 설명은 조금 다릅니다.

In order to make the value of an environment variable accessible in the browser, Next.js can "inline" a value, at build time, into the js bundle that is delivered to the client, replacing all references to process.env.[variable] with a hard-coded value.

브라우저에는 환경 변수를 읽는 기능이 없습니다. NEXT_PUBLIC_ 이 하는 일은 빌드할 때 그 값을 코드에 박아 넣는 것입니다. process.env.NEXT_PUBLIC_ANALYTICS_ID 는 번들에서 'abcdefghijk' 라는 문자열로 바뀝니다.

빌드 때 박힌 값은 배포 환경을 따라오지 않는다

이 성질에서 처음의 문제가 설명됩니다. 문서가 같은 자리에 주의로 적어 둔 내용입니다.

After being built, your app will no longer respond to changes to these environment variables. ... if you build and deploy a single Docker image to multiple environments, all NEXT_PUBLIC_ variables will be frozen with the value evaluated at build time.

한 번 빌드한 이미지를 여러 환경에 올리면 NEXT_PUBLIC_ 값들은 빌드 때의 값으로 얼어붙습니다. 스테이징에 올려도 빌드할 때 쓴 값이 그대로 나옵니다. 문서는 런타임 값이 필요하면 클라이언트에 값을 내려 줄 API 를 따로 만들라고 안내합니다.

서버 쪽 값은 다릅니다. 문서는 동적 렌더링 중에는 서버에서 환경 변수를 안전하게 읽을 수 있다고 하면서, connection() 처럼 요청 시점을 표시하는 API 를 함께 보여 줍니다. 서버에서 읽는 값은 런타임 값이고, 브라우저로 내보내는 값은 빌드 타임 값이라는 구분이 핵심입니다.

이름을 변수로 조립하면 바뀌지 않는다

인라인은 빌드 도구가 코드의 글자를 그대로 찾아 바꾸는 방식입니다. 그래서 문서는 이런 코드가 치환되지 않는다고 적습니다.

// 치환되지 않는다
const varName = "NEXT_PUBLIC_ANALYTICS_ID";
setupAnalyticsService(process.env[varName]);

const env = process.env;
setupAnalyticsService(env.NEXT_PUBLIC_ANALYTICS_ID);

설정 값을 객체로 모아 두거나 변수로 키를 만드는 구조가 브라우저 쪽 코드에 들어가면 값이 undefined 가 됩니다. 브라우저에서 쓰는 환경 변수는 process.env.NEXT_PUBLIC_XXX 형태 그대로 쓰는 것이 안전합니다.

접두어가 없는 값이 클라이언트로 갔을 때

반대 방향도 봐야 합니다. 접두어가 없는 변수는 문서에 따르면 Node.js 환경에서만 읽을 수 있습니다. 클라이언트는 다른 환경이라 값이 없습니다.

문제는 그 사실이 조용하다는 점입니다. 서버 컴포넌트 문서는 접두어 없는 변수가 클라이언트 번들에서 빈 문자열로 바뀐다고 적습니다. 비밀 키가 브라우저로 새지는 않지만, 그 값을 쓰는 함수는 오류 없이 이상하게 동작합니다. 인증 헤더가 빈 문자열인 채로 요청이 나가는 식입니다.

그래서 문서는 서버 전용 코드의 맨 위에 import "server-only" 를 두는 방법을 권합니다. 그 파일이 클라이언트 모듈 트리에 들어가면 빌드가 실패합니다. 조용히 틀리는 대신 빌드에서 멈추는 쪽이 낫다고 봅니다.

어느 파일의 값이 이기는가

여러 .env 파일을 두면 우선순위가 헷갈립니다. 문서는 순서를 명시합니다. 위에서부터 찾다가 값을 발견하면 멈춥니다.

  1. process.env
  2. .env.$(NODE_ENV).local
  3. .env.local (NODE_ENVtest 일 때는 읽지 않음)
  4. .env.$(NODE_ENV)
  5. .env

NODE_ENV 에 허용되는 값은 production, development, test 셋입니다. next devdevelopment, 나머지 명령은 production 으로 자동 설정됩니다.

세 번째 줄의 괄호가 실무에서 자주 걸립니다. 테스트 환경에서는 .env.local 을 읽지 않습니다. 문서는 테스트가 누구에게나 같은 결과를 내도록 하려는 의도라고 설명합니다. 로컬에서만 되던 테스트가 CI 에서 다르게 도는 원인이 여기 있을 수 있습니다.

/src 디렉터리를 쓰는 프로젝트라면 .env.* 파일은 src 안이 아니라 프로젝트 루트에 둡니다. Next.js 런타임 밖에서(ORM 설정 파일이나 테스트 러너에서) 같은 값을 읽어야 한다면 @next/env 패키지를 쓰라고 문서가 안내합니다.

값을 두기 전에 묻는 두 가지

질문 답에 따른 자리
이 값을 브라우저가 읽어야 하는가 읽어야 하면 NEXT_PUBLIC_, 아니면 접두어 없이
환경마다 달라야 하는가 NEXT_PUBLIC_ 이면 환경마다 따로 빌드하거나, 런타임에 내려 줄 경로를 만든다

두 답이 「브라우저가 읽어야 하고, 환경마다 달라야 한다」로 겹치는 값이 처음의 분석 도구 ID 였습니다. 그런 값은 한 이미지를 여러 환경에 올리는 배포와 맞지 않습니다. 환경마다 빌드를 따로 하거나, 서버가 응답에 실어 내려 주는 구조로 바꿔야 합니다.

여기까지가 확실한 부분

NEXT_PUBLIC_ 이 빌드 시점 인라인이라는 점, 한 이미지를 여러 환경에 올릴 때 값이 얼어붙는다는 주의, 변수로 조립한 접근이 치환되지 않는다는 점, 접두어 없는 값이 클라이언트 번들에서 빈 문자열이 된다는 점, .env 로드 순서와 테스트 환경에서 .env.local 을 읽지 않는다는 점, /src 디렉터리에서의 위치와 @next/env 안내는 Next.js 16 문서 기준입니다. 배포 도구마다 빌드 시점에 어떤 환경 변수가 주입되는지는 그 플랫폼의 설정에 달려 있어서, 값이 언제 결정되는지는 그쪽 문서도 함께 확인해야 합니다.

마지막 수정:

공유하실 때는 출처(Duolabs)와 원문 주소를 표시해 주세요.