개발자
류준열
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-env
와 process.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 내부 구조 뜯어보기
기존 사용법 요약
공식 문서 기준 사용법은 간단하다.
- 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>
);
}
- 사용하는 곳에서 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 원리 요약
- process.env는 Node.js환경에서는 런타임에 접근 가능하다.
- 위 특성을 사용하여 서버컴포넌트에서 process.env에 접근하고 클라이언트에 전달한다.
- 런타임 환경변수를 적용할 수 있게 된다.