cross-env
Node.js 프로젝트에서 환경 변수를 설정하는 건 일상적인 작업이다. 개발 환경에서는 NODE_ENV=development, 프로덕션에서는 NODE_ENV=production을 주입해서 동작을 분기하는 패턴이 가장 흔하다.
{
"scripts": {
"start:dev": "NODE_ENV=development node app.js",
"start:prod": "NODE_ENV=production node app.js"
}
}
Linux나 macOS에서는 이 방식이 잘 동작한다. 그런데 Windows에서 실행하면 에러가 난다.
'NODE_ENV'은(는) 내부 또는 외부 명령, 실행할 수 있는 프로그램, 또는 배치 파일이 아닙니다.
이유는 간단하다. 환경 변수를 설정하는 문법이 OS마다 다르기 때문이다.
| OS | 문법 |
|---|---|
| Linux / macOS (bash) | NODE_ENV=production node app.js |
| Windows (cmd) | set NODE_ENV=production && node app.js |
| Windows (PowerShell) | $env:NODE_ENV="production"; node app.js |
팀원 중 누군가 Windows를 쓰고 있다면, 혹은 CI 환경이 Linux인데 로컬은 Windows라면, 하나의 package.json 스크립트로 모든 환경을 커버할 수 없다. 이걸 해결하는 게 cross-env다.
cross-env가 하는 일
cross-env는 OS에 관계없이 동일한 문법으로 환경 변수를 설정해주는 CLI 도구다. 내부적으로 현재 OS를 감지하고, 해당 OS에 맞는 방식으로 환경 변수를 주입한 뒤 자식 프로세스를 실행한다.
'NODE_ENV'은(는) 내부 또는 외부 명령, 실행할 수 있는 프로그램, 또는 배치 파일이 아닙니다.
이렇게 하면 Linux, macOS, Windows 어디서든 동일하게 동작한다.
설치
개발 의존성으로 설치한다. 런타임에는 필요 없고 npm 스크립트 실행 시에만 사용되기 때문이다.
{
"scripts": {
"start:dev": "cross-env NODE_ENV=development node app.js",
"start:prod": "cross-env NODE_ENV=production node app.js"
}
}
동작 원리
cross-env의 내부 동작은 생각보다 단순하다.
- 커맨드라인 인자에서
KEY=VALUE형태의 환경 변수를 파싱한다 - 나머지 부분을 실행할 명령어로 분리한다
- Node.js의
child_process.spawn()으로 자식 프로세스를 생성한다 - 이때
env옵션에 파싱한 환경 변수를 병합해서 전달한다
핵심은 spawn()의 env 옵션이다. 이 옵션으로 전달된 환경 변수는 OS 문법과 무관하게 자식 프로세스에 주입된다. OS별 문법 차이를 Node.js API 레벨에서 우회하는 셈이다.
npm install --save-dev cross-env
stdio: 'inherit'로 부모 프로세스의 stdin/stdout/stderr를 그대로 상속하기 때문에, 자식 프로세스의 출력이 터미널에 자연스럽게 표시된다. shell: true는 Windows에서 .cmd 확장자 파일(npm 바이너리 등)을 실행하기 위해 필요하다.
여러 환경 변수 설정
공백으로 구분해서 여러 환경 변수를 한 번에 설정할 수 있다.
// cross-env 내부 동작의 단순화된 버전
const { spawn } = require('child_process');
function crossEnv(args) {
const envVars = {};
let commandStart = 0;
// KEY=VALUE 파싱
for (let i = 0; i < args.length; i++) {
const match = args[i].match(/^([^=]+)=(.*)$/);
if (match) {
envVars[match[1]] = match[2];
commandStart = i + 1;
} else {
break;
}
}
const command = args[commandStart];
const commandArgs = args.slice(commandStart + 1);
// 현재 환경 변수에 새 변수를 병합
const env = { ...process.env, ...envVars };
// OS에 맞는 방식으로 자식 프로세스 실행
spawn(command, commandArgs, {
env,
stdio: 'inherit',
shell: true
});
}
값에 공백이 포함된 경우
환경 변수 값에 공백이 있으면 따옴표로 감싸야 한다. 단, OS별 따옴표 처리 차이도 있기 때문에 cross-env가 이 부분도 처리해준다.
{
"scripts": {
"test": "cross-env NODE_ENV=test DB_HOST=localhost DB_PORT=3306 jest",
"build": "cross-env NODE_ENV=production API_URL=https://api.example.com webpack"
}
}
cross-env-shell
cross-env는 기본적으로 spawn()으로 명령어를 실행한다. 그런데 셸 내장 기능(파이프, 리다이렉트, && 체이닝 등)을 사용해야 할 때는 cross-env-shell을 대신 사용한다.
{
"scripts": {
"greet": "cross-env GREETING=\"Hello World\" node app.js"
}
}
차이점은 이렇다:
| cross-env | cross-env-shell | |
|---|---|---|
| 실행 방식 | spawn() (직접 실행) | spawn() + shell: true (셸 경유) |
| 환경 변수 확장 | 지원 안 함 | $NODE_ENV / %NODE_ENV% 자동 변환 |
| 셸 기능 | 제한적 | 파이프, 리다이렉트, && 모두 사용 가능 |
| 성능 | 셸 프로세스 없이 직접 실행 | 셸을 한 번 거침 |
cross-env-shell은 환경 변수 참조도 처리해준다. $NODE_ENV(Unix)와 %NODE_ENV%(Windows) 문법을 알아서 변환한다.
{
"scripts": {
"log-build": "cross-env-shell NODE_ENV=production \"webpack --config webpack.prod.js > build.log 2>&1\"",
"check-and-build": "cross-env-shell NODE_ENV=production \"eslint src && webpack\""
}
}
이 스크립트는 어떤 OS에서든 production을 출력한다.
실전 사용 패턴
테스트 환경 분리
테스트를 실행할 때 개발 DB와 분리된 테스트 DB를 사용하는 건 기본이다. cross-env로 NODE_ENV=test를 주입하면 설정 파일에서 이를 읽어 DB 연결 정보를 분기할 수 있다.
{
"scripts": {
"echo-env": "cross-env-shell NODE_ENV=production \"echo $NODE_ENV\""
}
}
{
"scripts": {
"test": "cross-env NODE_ENV=test jest --config jest.config.ts",
"test:e2e": "cross-env NODE_ENV=test jest --config jest-e2e.config.ts",
"test:watch": "cross-env NODE_ENV=test jest --watch"
}
}
빌드 모드 분기
프론트엔드 빌드에서 번들러의 mode를 환경 변수로 제어하는 패턴이다.
// config.ts
const config = {
database: {
host: process.env.NODE_ENV === 'test' ? 'localhost' : process.env.DB_HOST,
port: process.env.NODE_ENV === 'test' ? 3307 : Number(process.env.DB_PORT),
database: process.env.NODE_ENV === 'test' ? 'my_app_test' : process.env.DB_NAME,
}
};
ANALYZE=true처럼 커스텀 환경 변수를 추가로 넘겨서 번들 분석 플러그인을 조건부로 활성화하는 것도 가능하다.
로그 레벨 제어
{
"scripts": {
"build:dev": "cross-env NODE_ENV=development webpack --config webpack.dev.js",
"build:prod": "cross-env NODE_ENV=production webpack --config webpack.prod.js",
"build:analyze": "cross-env NODE_ENV=production ANALYZE=true webpack --config webpack.prod.js"
}
}
대안과 비교
dotenv
dotenv는 .env 파일에서 환경 변수를 로드하는 라이브러리다. cross-env와 목적이 다르다.
| cross-env | dotenv | |
|---|---|---|
| 동작 시점 | npm 스크립트 실행 시 (CLI) | 런타임 (코드 내부) |
| 설정 위치 | package.json scripts | .env 파일 |
| 주요 용도 | OS 간 호환성 | 환경별 설정 파일 관리 |
| 민감 정보 | 부적합 (package.json에 노출) | 적합 (.gitignore로 제외) |
실제로는 둘을 함께 쓰는 경우가 많다. cross-env로 NODE_ENV를 설정하고, dotenv로 해당 환경에 맞는 .env 파일을 로드하는 방식이다.
{
"scripts": {
"start:debug": "cross-env NODE_ENV=development LOG_LEVEL=debug node app.js",
"start:prod": "cross-env NODE_ENV=production LOG_LEVEL=warn node app.js"
}
}
env-cmd
env-cmd는 파일 기반으로 환경 변수를 로드하는 CLI 도구다. cross-env처럼 크로스 플랫폼을 지원하면서도, 환경 변수가 많을 때 파일로 관리할 수 있다는 장점이 있다.
{
"scripts": {
"start:dev": "cross-env NODE_ENV=development node -r dotenv/config app.js"
}
}
환경 변수가 1~3개 정도면 cross-env가 간편하고, 10개 이상이면 env-cmd나 dotenv를 쓰는 게 가독성이 좋다.
cross-env가 필요 없는 경우
Node.js 자체 기능으로 해결할 수 있는 상황도 있다. --env-file 플래그가 Node.js 20.6.0부터 추가되었다.
{
"scripts": {
"start:dev": "env-cmd -f .env.development node app.js"
}
}
또한 팀원 전원이 Unix 계열 OS를 사용하고 CI도 Linux라면 굳이 cross-env를 쓸 필요가 없다. 하지만 오픈소스 프로젝트처럼 기여자의 OS를 예측할 수 없는 경우에는 cross-env를 쓰는 게 안전하다.
정리
cross-env는 "환경 변수 설정 문법이 OS마다 다르다"는 단순한 문제를 해결하는 도구다. 내부적으로는 child_process.spawn()의 env 옵션을 활용해서 OS 문법 차이를 우회한다. 코드량도 적고 원리도 간단하지만, 크로스 플랫폼 호환성이 필요한 모든 Node.js 프로젝트에서 사실상 표준처럼 사용되고 있다.
관련 문서
- pnpm workspace - 모노레포 의존성 관리
- tsc-alias - TypeScript 경로 별칭 변환
- Winston 로거 - 환경별 로그 레벨 분기