환경변수는 보통 민감한 정보를 감추기 위해 주로 사용하곤 합니다. 그만큼 제대로 이해하고 사용해야하기도 하고요.
오늘은 환경변수에 대해 필수적으로 알아야하는 부분을 정리해놓도록 하겠습니다.
환경 변수는 어디에 등록되는 것인가?
환경 변수는 OS에 등록됩니다. 컴퓨터의 OS요.
즉, 운영체제 레벨의 환경변수로 저장되어 프로세스에 전달됩니다.
예를 들어 아래와 같은 명령어를 실행하면,
NODE_ENV=production PORT=3000 node server.js
NODE_ENV=production, PORT=3000 이라는 환경 변수값이 Node 프로세스로 전달되며 시작됩니다.
참고) 운영체제(OS)는 프로세스를 실행할 때, 환경(Environment Block) 이라는 구조체를 전달합니다.
따라서 정확히는 OS 환경 변수라는 것은 컴퓨터의 환경 변수가 아니라, 프로세스의 환경 변수로 이해하는 것이 적절합니다.
환경 변수는 어떻게 노출되는가?
해당 프로세스에서 자체적으로 OS에 요청을 해서 환경 변수를 가져와야만 합니다. 그렇지 않으면 노출되지 않습니다.
Node의 경우에는 이를 process.env 객체를 통해 노출해줍니다. 즉, OS에서 환경 변수의 값을 가져와서 process.env 객체로 접근할 수 있도록 만들어주는겁니다. 그럼 프로그래머는 아래와 같은 코드로 해당 환경 변수에 접근할 수 있게 됩니다.
console.log(process.env.NODE_ENV); // "production"
console.log(process.env.PORT); // "3000"
그럼 브라우저에서는 환경 변수를 어떻게 사용하는가?
프론트엔드 개발을 좀 해보신 분들이라면, 브라우저 환경에서 개발을 진행할 때도 환경 변수를 사용한 경험이 있으실겁니다.
하지만 본질적으로, 브라우저는 OS 환경 변수에 대한 개념을 가지고 있지 않습니다. 왜냐하면 브라우저는 오직 html, css, js 파일을 받아와서 실행 및 렌더링을 진행하기 때문입니다.
그럼에도 불구하고 브라우저 환경에서 환경 변수를 사용했다고 '착각' 하는 이유는 빌드 도구(webpack, vite 등)가
빌드 타임에 환경 변수의 값을 문자열로 치환해서 넣어주기 때문입니다.
//before (원본 코드)
console.log(process.env.NEXT_PUBLIC_API_URL);
//after (빌드 과정을 거친 이후 코드)
console.log("https://api.example.com"); //해당 값으로 대체됨
치환된 환경 변수는 브라우저에서 받아오는 js 파일을 샅샅이 뒤지다보면 결국 파악할 수 있습니다.
하지만 모든 환경 변수가 치환되는 것은 아닙니다. 명시적으로 개발자가 지정해둔 환경변수만 치환돼요.
어떤 환경 변수가 빌드타임에 치환되는가?
이는 어떤 빌드도구를 사용하느냐에 따라 다릅니다.
Vite를 사용한다면 import.meta.env 라는 특수한 객체를 통해 특정 상수를 노출합니다. (또한 VITE_라는 prefix 사용)
Next.js (내부적으로 Webpack)을 사용한다면 process.env. 중에서도 NEXT_PUBLIC 라는 prefix를 사용한 환경 변수만 치환합니다.
이해를 돕기 위한 예시) 센트리에서 자체적으로 환경 변수를 세팅하는 과정
센트리에서는 명시적으로 environment 값을 지정해주지 않으면 자체적으로 환경 변수를 뽑아와서 그 값을 결정합니다.
이 과정을 통해 환경 변수의 사용 흐름을 이해해봅시다. (버셀 플랫폼을 통해 next 프로젝트를 배포하는 상황을 가정합니다.)
1. 버셀에서 프로젝트를 fork를 떠가서 빌드(next build)될 때, NODE_ENV='production'을 넘기며 node 프로세스가 실행됩니다.
이는 Next 프레임워크의 동작 방식입니다. (만약 next dev 였다면 NODE_ENV='development'을 넘깁니다.)
2. 이때, 버셀 플랫폼에서는 자체적으로 어떤 프레임워크를 사용하는지 판단하고 환경 변수를 추가로 같이 주입하며 node 프로세스를 실행합니다. Next.js를 사용하고 있다면 NEXT_PUBLIC_VERCEL_ENV = 'production' | 'preview' | 'development' 를 넘깁니다.
3. 그럼 결국 버셀 플랫폼에서 next build가 실행되고 있는 node 프로세스에는 NODE_ENV, NEXT_PUBLIC_VERCEL_ENV 값 등이 환경변수로 같이 넘어가서 등록되어 있습니다. 이는 코드에서 process.env. 객체를 통해 접근가능합니다. (node에서 제공)
4. 빌드되는 과정 속에서 센트리의 sdk도 빌드되고 있습니다.
5. 센트리에는 아래와 같은 코드가 존재합니다.
export function getVercelEnv(isClient: boolean): string | undefined {
const vercelEnvVar = isClient ? process.env.NEXT_PUBLIC_VERCEL_ENV : process.env.VERCEL_ENV;
return vercelEnvVar ? `vercel-${vercelEnvVar}` : undefined;
}
즉, node 프로세스로 넘겨진 환경 변수를 읽으려고 시도합니다. NEXT_PUBLIC_VERCEL_ENV 값이 존재하니, 결국 빌드 과정에서process.env.NEXT_PUBLIC_VERCEL_ENV 값은 문자열 리터럴 값으로 치환되어 있을겁니다. (참고로 VERCEL_ENV 값도 버셀에서 node 실행하면서 넘겨주는 값입니다.)
6. sentry의 config에서 environment 값이 지정되어 있지 않으면, 위의 getVercelEnv 함수를 실행하여 값을 가져와서 environment 값에 자체적으로 넣어줍니다. 따라서 실제로 sentry의 environment를 확인해보면, development, vercel-production 2가지가 존재하는 것을 확인할 수 있습니다.

마무리
결국 가장 중요한 2가지 개념은 다음과 같습니다.
1. 환경 변수는 프로세스에 등록되고, 각 프로세스가 노출하는 창구(node의 경우 process.env. 와 같이 )를 열어준다는 점.
2. 브라우저에서 사용하는 환경 변수는 사실 빌드 타임에 문자열 리터럴로 치환되며, 빌드 도구가 치환할 환경 변수를 결정한다는 점.
따라서 이런 부분들을 잘 구분하면 브라우저에서 실행되는 코드와 서버에서 실행되는 코드를 분리하여 환경 변수를 Public으로 정의해둘지를 결정해둘 수 있습니다. (Public으로 정의한다 == 문자열 리터럴로 치환되도록 만든다)
예를 들어 센트리의 dsn을 설정하는 코드가 존재하는 instrumentation-client.ts 파일은 오직 클라이언트(브라우저)에서만 실행됩니다.
따라서 아래와 같이 반드시 문자열로 치환해서 사용할 수 있도록, (next 프레임워크의 방식대로) NEXT_PUBLIC을 붙여야 합니다.
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
...후략
})
아니면 dsn의 값이 제대로 치환되지 않고 브라우저에서 해당 코드가 실행될 때 undefined 값이 뜰겁니다.
생각보다는 글이 길어졌네요.
긴 글 읽어주셔서 감사합니다.
참고)
버셀의 프레임워크에 따른 환경 변수 : https://vercel.com/docs/environment-variables/framework-environment-variables
Framework environment variables
Framework environment variables are automatically populated by the Vercel, based on your project's framework.
vercel.com
센트리에서 환경을 세팅하는 핵심 코드 : https://github.com/getsentry/sentry-javascript/blob/499b79a179197f48e9bdd38f1c91255cccac80f8/packages/vercel-edge/src/utils/vercel.ts#L4
sentry-javascript/packages/vercel-edge/src/utils/vercel.ts at 499b79a179197f48e9bdd38f1c91255cccac80f8 · getsentry/sentry-javas
Official Sentry SDKs for JavaScript. Contribute to getsentry/sentry-javascript development by creating an account on GitHub.
github.com