개발자
류준열

next-runtime-env 원리 파헤치기

Next와 React에서는 환경변수가 빌드타임에 주입된다.
하지만 쿠버네티스에서 주입하는 환경변수는 도커 이미지가 빌드 된 후, 런타임에 주입된다.

이럴땐 process.env로 접근한 값이 undefined가 되는 문제가 발생한다. 빌드 시점에 환경변수가 없었기 때문에, Next.js의 번들에는 그 값이 포함되지 않았기 때문이다. React도 마찬가지다.

이 문제를 해결하려면 단순히 빌드 전에 환경변수를 주입하면 된다.

하지만 빌드(배포)없이 환경변수를 바꾸고 싶다는 요구가 있다면 상황은 달라진다.

그럴땐 next-runtime-env라는 것을 사용하면 된다.

런타임 환경변수 (You saved my life, 내가 적은 답변이다.)

런타임 환경변수 실험해보기

나는 쿠버네티스를 잘 몰라서 docker-compose로 대신했다.

1. 환경변수 주입

docker-compose에 다음과 같이 런타임 환경변수를 설정했다.

version: "3"

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: nextjs-app
    environment:
      NEXT_PUBLIC_API_URL: https://koreanjson.com/

    ports:
      - "3000:3000"

2. 클라이언트 컴포넌트 작성

이제 클라이언트에서 next-runtime-envprocess.env 값을 비교해본다.

"use client";

import Image from "next/image";
import styles from "./page.module.css";
import { env } from "next-runtime-env";

export default function Home() {
  return (
    <div className={styles.page}>
      <main className={styles.main}>
        <div>런타임 환경변수: {env("NEXT_PUBLIC_API_URL")}</div>
        <div>빌드타임 환경변수: {process.env.NEXT_PUBLIC_API_URL}</div>
      
       ...
    </div>
  );
}

결과는 아래와 같다.

  • 런타임 환경변수(next-runtime-env): 정상 출력
  • 빌드타임 환경변수(process.env): undefined

next-runtime-env 내부 구조 뜯어보기

기존 사용법 요약

공식 문서 기준 사용법은 간단하다.

  1. layout.tsx의 <head> 태그에 <PublicEnvScript /> 삽입
// app/layout.tsx
import { PublicEnvScript } from 'next-runtime-env';

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <PublicEnvScript />
      </head>
      <body>
        {children}
      </body>
    </html>
  );
}
  1. 사용하는 곳에서 env("NEXT_PUBLIC_XXX") 호출
// app/client-page.tsx
'use client';
import { env } from 'next-runtime-env';

export default function SomePage() {
  const NEXT_PUBLIC_FOO = env('NEXT_PUBLIC_FOO');
  return <main>NEXT_PUBLIC_FOO: {NEXT_PUBLIC_FOO}</main>;
}

PublicEnvScript의 역할

export const PublicEnvScript: FC<PublicEnvScriptProps> = ({ nonce }) => {
  noStore(); // Opt into dynamic rendering

  // This value will be evaluated at runtime
  const publicEnv = getPublicEnv();

  return <EnvScript env={publicEnv} nonce={nonce} />;
};

여기서 핵심은 getPublicEnv() 함수다.

getPublicEnv는 뭘 가져오는가?

import { ProcessEnv } from '../typings/process-env';

/**
 * Gets a list of environment variables that start with `NEXT_PUBLIC_`.
 */
export function getPublicEnv() {
  const publicEnv = Object.keys(process.env)
    .filter((key) => /^NEXT_PUBLIC_/i.test(key))
    .reduce(
      (env, key) => ({
        ...env,
        [key]: process.env[key],
      }),
      {} as ProcessEnv,
    );

  return publicEnv;
}
  • process.env에서 NEXT_PUBLIC_으로 시작하는 키만 필터링해서 객체로 만든다.
  • 서버컴포넌트에서 실행되기 때문에 런타임 환경변수 접근이 가능하다.

EnvScript는 어떻게 window에 값을 넣는가?

/**
 * Sets the provided environment variables in the browser. If an nonce is
 * available, it will be set on the script tag.
 *
 * Usage:
 * ```ts
 * <head>
 *   <EnvScript env={{ NODE_ENV: 'test', API_URL: 'http://localhost:3000' }} />
 * </head>
 * ```
 */
export const EnvScript: FC<EnvScriptProps> = ({ env, nonce }) => {
  let nonceString: string | undefined;

  // XXX: Blocked by https://github.com/vercel/next.js/pull/58129
  // if (typeof nonce === 'object' && nonce !== null) {
  //   // It's strongly recommended to set a nonce on your script tags.
  //   nonceString = headers().get(nonce.headerKey) ?? undefined;
  // }

  if (typeof nonce === 'string') {
    nonceString = nonce;
  }

  return (
    <Script
      strategy="beforeInteractive"
      nonce={nonceString}
      dangerouslySetInnerHTML={{
        __html: `window['__ENV'] = ${JSON.stringify(env)}`,
      }}
    />
  );
};
  • 위 스크립트는 브라우저가 HTML을 파싱하기 전에 실행된다.
  • 즉, 클라이언트 환경에 진입하기 전, 런타임 환경변수를 window["__ENV"]에 미리 주입하는 것이다.

=> 서버 컴포넌트에서 런타임 환경변수를 읽어서 클라이언트의 window 객체에 스크립트로 넘겨준다.

콘솔창에서 window 객체에 저장된 환경변수를 확인 할 수 있다.

env는 어떻게 동작할까?

export function env(key: string): string | undefined {
  if (isBrowser()) {
    if (!key.startsWith('NEXT_PUBLIC_')) {
      throw new Error(
        `Environment variable '${key}' is not public and cannot be accessed in the browser.`,
      );
    }

    return window["__ENV"][key];
  }

  noStore();

  return process.env[key];
}
  • 브라우저 환경이면 `window["__ENV"]에서 값을 꺼내고,
  • 서버 환경이면 기존 방식대로 process.env를 사용한다.

next-runtime-env 원리 요약

  1. process.env는 Node.js환경에서는 런타임에 접근 가능하다.
  2. 위 특성을 사용하여 서버컴포넌트에서 process.env에 접근하고 클라이언트에 전달한다.
  3. 런타임 환경변수를 적용할 수 있게 된다.