개발자
류준열

next-runtime-env 원리 파헤치기

Next와 React에서는 환경변수가 빌드타임에 주입된다. 그런데 쿠버네티스에서 주입하는 환경변수는 도커 이미지가 빌드 된 후, 런타임에 주입된다. 이렇게 주입된 환경변수는 undefined가 된다. 이걸 해결하려면 빌드전에 환경변수를 주입하면 된다. (react도 마찬가지다.)

하지만 빌드없이 환경변수를 변경하기 위해 런타임 환경변수를 이용하는 상황이라면 어떻게 해야 할까?

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

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

런타임 환경변수 재현해보기

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

Next.js와 dockerfile을 간단히 만들고 docker-compose.yaml에 환경변수를 다음과 같이 넣어주었다.

version: "3"

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

    ports:
      - "3000:3000"

이렇게 하고 RCC를 만들었다.

"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 까보기

사용법

next-runtime-env의 사용법을 보면 최상단 layout.tsx의 head태그에 <PublicEnvScript />를 삽입하고 런타임 환경변수를 사용하는 곳에서 env 함수를 사용하면 된다.

// 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>
  );
}
// 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>;
}

<PublickEnvScript />env 함수를 까보자.

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와 EnvScript도 살펴보자.

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_으로 필터해서 가공 후 리턴한다. (process.env로 런타임 환경변수에 어떻게 접근한거지? 라는 의문이 드는데 일단 넘어간다.)

EnvScript

/**
 * 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)}`,
      }}
    />
  );
};

EnvScriptgetPublicEnv으로 추출한 환경변수를 window 객체에 넣어주는 Next Script 이다.

정리해보면 PublicEnvScript는 그냥 process.env에 접근해서 환경변수를 뽑아내고 이걸 window 객체에 넣어주는 스크립트이다.

그래서 콘솔창에서 `window["__ENV"]를 입력하면 환경변수를 볼 수 있다.

그럼 이제 env도 보자.

env

환경변수를 사용할 곳에서는 env 함수를 사용한다.

단순히 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];
}

그럼 어떻게 Next.js에서는 접근하지 못하는 런타임 환경변수에 process.env로 접근한걸까??

next-runtime-env는 어떻게 런타임 환경변수를 읽는가?

Next.js의 클라이언트 컴포넌트는 런타임 환경변수를 읽을 수 없지만, 서버컴포넌트는 런타임 환경변수를 읽을 수 있다.

next-runtime-env는 서버에서 런타임 환경변수를 읽어 클라이언트에 window객체로 전달한다.


// 최상단 layout.tsx

export default function RootLayout({..}){
  const publicEnv = getPublicEnv(); // 서버에서 런타임 환경변수 가져오기

  return (
    <html lang="en">
      <head>
        <Script strategy="beforeInteractive" 
          dangerouslySetInnerHTML={{__html: 
          `window['${PUBLIC_ENV_KEY}'] = ${JSON.stringify(env)}`,
        }}/>
      </head>
      <body className={`${geistSans.variable} ${geistMono.variable}`}>
        {children}
      </body>
    </html>
	)}

그리고 env함수에서는 window객체에 저장된 환경변수들을 빼온다.

 return window['__ENV'][key];

요약

  1. Next.js의 서버 컴포넌트는 런타임 환경변수에 접근할 수 있지만, 클라이언트 컴포넌트에서는 런타임 환경변수에 접근하지 못함.

  2. next-runtime-env는 Next 서버가 실행될때 최상단 layout.tsx에서 환경변수에 접근해서 NEXT_PUBLIC_으로 시작하는 환경변수들을 다 window 객체안에 넣는다.

  3. 클라이언트 컴포넌트가 실행되면 window 객체안에서 런타임 환경변수들을 빼온다.