npm install을 실행했는데 갑자기 터미널이 빨간 글씨로 도배되는 경험, 해본 적 있는가?
error engine Unsupported engine
error Found: [email protected]
error Required: node@^18.0.0
error at /Users/dev/project-a/package.json
지난달에 정확히 이 상황을 겪었다. 회사 레거시 프로젝트는 Node.js 18을 요구하고, 사이드 프로젝트에서 쓰던 Next.js 15는 Node.js 20 이상이 필요했다. Homebrew로 설치한 Node.js는 시스템 전체에 딱 하나의 버전만 유지하니까, 프로젝트를 바꿀 때마다 brew uninstall하고 다시 설치하는 짓을 반복하고 있었다. 솔직히 말하면 한 3번쯤 반복하다가 “이건 아니다” 싶었다.
이 글에서는 nvm(Node Version Manager)을 사용해서 Mac 터미널 환경에서 Node.js 버전 충돌 문제를 깔끔하게 해결하는 방법을 정리한다. 단순 설치법이 아니라, 프로젝트 디렉터리에 진입하면 자동으로 버전이 전환되는 설정까지 다룬다.
📑 목차
Node.js 버전 충돌이 발생하는 원인
Node.js 생태계는 버전에 민감하다. 라이브러리들이 특정 Node.js 버전의 API에 의존하기 때문에, package.json의 engines 필드로 요구 버전을 명시하는 경우가 많다.
{
"name": "legacy-admin-panel",
"version": "2.4.1",
"engines": {
"node": ">=18.0.0 <19.0.0",
"npm": ">=9.0.0"
},
"dependencies": {
"express": "^4.18.2",
"node-sass": "^8.0.0",
"webpack": "^5.88.0"
}
}
위처럼 node-sass 같은 네이티브 바이너리 모듈은 Node.js 메이저 버전이 바뀌면 아예 컴파일이 안 된다. Node.js 20에서 node-sass@8을 설치하면 이런 에러가 터진다:
$ npm install
npm ERR! code 1
npm ERR! path /Users/dev/project/node_modules/node-sass
npm ERR! command failed
npm ERR! command sh -c node scripts/build.js
npm ERR! Building: /usr/local/bin/node /Users/dev/project/node_modules/node-gyp/bin/node-gyp.js rebuild
npm ERR! gyp ERR! build error
npm ERR! gyp ERR! stack Error: `make` failed with exit code: 2
npm ERR! gyp ERR! System Darwin 24.3.0
npm ERR! gyp ERR! node -v v20.11.1
npm ERR! gyp ERR! node-gyp -v v10.0.1
# 또 다른 흔한 에러: global 패키지 충돌
$ npx create-next-app@latest my-app
Need to install the following packages:
[email protected]
Ok to proceed? (y)
Error: Next.js 15 requires Node.js >= 20.0.0
You are using Node.js 18.19.0
Please upgrade Node.js to continue.
문제의 핵심은 간단하다. 프로젝트마다 요구하는 Node.js 버전이 다른데, 시스템에는 하나의 버전만 설치되어 있다는 것이다. Python에서 pyenv가 필요한 이유와 동일하다.
Homebrew 단일 설치의 한계
Mac에서 Node.js를 처음 설치할 때 대부분 brew install node를 사용한다. 간편하지만, 이 방식에는 구조적인 한계가 있다.
# Homebrew로 설치된 Node.js 확인
$ which node
/opt/homebrew/bin/node
$ node -v
v22.12.0
$ ls -la /opt/homebrew/Cellar/node/
total 0
drwxr-xr-x 3 dev admin 96 1 15 14:22 .
drwxr-xr-x 87 dev admin 2784 3 20 09:11 ..
drwxr-xr-x 13 dev admin 416 1 15 14:22 22.12.0
# Homebrew는 한 번에 하나의 버전만 관리
# 다른 버전이 필요하면?
$ brew install node@18
$ brew unlink node
$ brew link node@18 --force --overwrite
# 다시 22로 돌아가려면...
$ brew unlink node@18
$ brew link node --force --overwrite
# 이걸 프로젝트 전환할 때마다 반복해야 합니다
Homebrew의 link/unlink은 심볼릭 링크를 교체하는 방식이라, 전환할 때마다 글로벌 패키지도 함께 깨진다. npm -g로 설치한 도구들이 갑자기 command not found가 되는 상황이 발생하는 거죠. 개인적으로 이 문내 때문에 30분을 날린 적이 있어서, 더 이상 이 방식은 추천하지 않다.
nvm 설치 및 쉘 설정
nvm은 Node.js 버전별로 독립된 디렉터리를 만들어서 관리하는 도구이다. ~/.nvm/versions/node/ 하위에 각 버전이 완전히 분리되어 설치된다.
설치 스크립트 실행
# 2026년 4월 기준 최신 v0.40.1
$ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
# 설치 스크립트 실행 후 출력 예시
=> Downloading nvm from git to '/Users/dev/.nvm'
=> Cloning into '/Users/dev/.nvm'...
=> Compressing and cleaning up git repository
=> Appending nvm source string to /Users/dev/.zshrc
=> Appending bash_completion source string to /Users/dev/.zshrc
=> Close and reopen your terminal to start using nvm or run the following:
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
설치 스크립트가 자동으로 ~/.zshrc(또는 ~/.bashrc)에 초기화 코드를 추가한다. 하지만 Mac의 기본 쉘이 zsh인 경우, 설정 파일이 여러 개 존재할 수 있어서 수동으로 확인하는 게 안전하다.
쉘 설정 파일 확인 및 수정
# 현재 사용 중인 쉘 확인
$ echo $SHELL
/bin/zsh
# zsh 사용자: ~/.zshrc 파일 확인
$ cat ~/.zshrc | grep -A 3 "NVM_DIR"
# 아래 3줄이 있어야 합니다
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
# 만약 없다면 직접 추가
$ cat >> ~/.zshrc << 'EOF'
# nvm 설정
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
EOF
# 설정 반영
$ source ~/.zshrc
# 설치 확인
$ nvm --version
0.40.1
# bash 사용자의 경우 ~/.bashrc 또는 ~/.bash_profile에 동일하게 추가
# Mac에서는 ~/.bash_profile이 로그인 쉘에서 로드됩니다
$ cat >> ~/.bash_profile << 'EOF'
# nvm 설정
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"
EOF
주의: ~/.zprofile과 ~/.zshrc를 혼동하는 경우가 있다. nvm 초기화 코드는 반드시 ~/.zshrc에 넣어야 한다. .zprofile은 로그인 시 한 번만 실행되므로, 새 터미널 탭을 열 때 nvm이 인식되지 않는 문내가 발생할 수 있다.
nvm 기본 사용법 — install, use, alias
Node.js 버전 설치
# 설치 가능한 LTS 버전 목록 확인
$ nvm ls-remote --lts
v18.19.0 (LTS: Hydrogen)
v18.19.1 (LTS: Hydrogen)
v18.20.0 (LTS: Hydrogen)
v18.20.1 (LTS: Hydrogen)
v18.20.2 (LTS: Hydrogen)
v18.20.3 (LTS: Hydrogen)
v18.20.4 (Latest LTS: Hydrogen)
v20.11.0 (LTS: Iron)
v20.11.1 (LTS: Iron)
v20.12.0 (LTS: Iron)
v20.12.1 (LTS: Iron)
v20.12.2 (LTS: Iron)
v20.18.0 (LTS: Iron)
v20.18.1 (Latest LTS: Iron)
v22.11.0 (LTS: Jod)
v22.12.0 (Latest LTS: Jod)
# 특정 버전 설치
$ nvm install 18.20.4
Downloading and installing node v18.20.4...
Downloading https://nodejs.org/dist/v18.20.4/node-v18.20.4-darwin-arm64.tar.xz...
######################################################################### 100.0%
Computing checksum with shasum -a 256
Checksums matched!
Now using node v18.20.4 (npm v10.7.0)
# 최신 LTS 설치 (버전 번호 대신 별칭 사용)
$ nvm install --lts
Installing latest LTS version.
Downloading and installing node v22.12.0...
Now using node v22.12.0 (npm v10.9.0)
# 특정 메이저 버전의 최신 설치
$ nvm install 20
Downloading and installing node v20.18.1...
Now using node v20.18.1 (npm v10.8.2)
버전 전환 및 확인
# 설치된 버전 목록
$ nvm ls
v18.20.4
v20.18.1
-> v22.12.0
default -> 22 (-> v22.12.0)
lts/* -> lts/jod (-> v22.12.0)
lts/hydrogen -> v18.20.4
lts/iron -> v20.18.1
lts/jod -> v22.12.0
# 버전 전환
$ nvm use 18
Now using node v18.20.4 (npm v10.7.0)
$ node -v
v18.20.4
# 기본 버전 설정 (새 터미널 열 때 자동 적용)
$ nvm alias default 20
default -> 20 (-> v20.18.1)
# 이제 새 터미널을 열면 v20.18.1이 기본
$ node -v
v20.18.1
# 특정 버전으로 일회성 실행 (현재 쉘 버전 변경 없이)
$ nvm exec 18 node -e "console.log(process.version)"
v18.20.4
$ node -v # 여전히 기본 버전
v20.18.1
# 더 이상 필요 없는 버전 제거
$ nvm uninstall 18.20.4
Uninstalled node v18.20.4
참고로 nvm use는 현재 터미널 세션에만 적용된다. 다른 터미널 탭에서는 여전히 default 버전이 활성화되어 있다. 이게 장점이기도 한데, 터미널 탭마다 서로 다른 Node.js 버전을 동시에 사용할 수 있다는 뜻이니까.
.nvmrc와 디렉터리 자동 전환 설정
프로젝트 루트에 .nvmrc 파일을 만들면, 팀원 전체가 동일한 Node.js 버전을 사용하도록 강제할 수 있다.
.nvmrc 파일 생성
# 프로젝트 디렉터리에서
$ cd ~/projects/legacy-admin
# 현재 사용 중인 버전을 .nvmrc로 저장
$ node -v > .nvmrc
$ cat .nvmrc
v18.20.4
# 또는 직접 작성 (v 접두어 없이도 동작)
$ echo "18" > .nvmrc
# .nvmrc가 있는 디렉터리에서 nvm use만 실행하면 자동 인식
$ nvm use
Found '/Users/dev/projects/legacy-admin/.nvmrc' with version <18>
Now using node v18.20.4 (npm v10.7.0)
하지만 매번 cd한 뒤에 nvm use를 수동으로 치는 건 번거롭다. 쉘 훅을 설정하면 디렉터리 이동 시 자동으로 버전이 전환된다.
zsh 자동 전환 설정
# ~/.zshrc에 아래 함수를 추가합니다
# nvm 초기화 코드 아래에 넣으세요
autoload -U add-zsh-hook
load-nvmrc() {
local nvmrc_path
nvmrc_path="$(nvm_find_nvmrc)"
if [ -n "$nvmrc_path" ]; then
local nvmrc_node_version
nvmrc_node_version=$(nvm version "$(cat "${nvmrc_path}")")
if [ "$nvmrc_node_version" = "N/A" ]; then
nvm install
elif [ "$nvmrc_node_version" != "$(nvm version)" ]; then
nvm use
fi
elif [ -n "$(PWD=$OLDPWD nvm_find_nvmrc)" ] && [ "$(nvm version)" != "$(nvm version default)" ]; then
echo "Reverting to nvm default version"
nvm use default
fi
}
add-zsh-hook chpwd load-nvmrc
load-nvmrc
# 설정 반영
$ source ~/.zshrc
이내 디렉터리를 이동하면 자동으로 버전이 전환된다:
# 실제 동작 확인
$ cd ~/projects/legacy-admin # .nvmrc: 18
Found '/Users/dev/projects/legacy-admin/.nvmrc' with version <18>
Now using node v18.20.4 (npm v10.7.0)
$ cd ~/projects/next-app # .nvmrc: 20
Found '/Users/dev/projects/next-app/.nvmrc' with version <20>
Now using node v20.18.1 (npm v10.8.2)
$ cd ~ # .nvmrc 없음 → default로 복귀
Reverting to nvm default version
Now using node v22.12.0 (npm v10.9.0)
이 설정 하나로 프로젝트 간 버전 충돌 문내가 완전히 해결된다. 내가 쓰면서 가장 만족스러운 부분이었다.
기존 Homebrew Node.js 정리
nvm을 설치한 뒤에도 Homebrew로 설치한 Node.js가 남아 있으면 which node 경로가 꼬일 수 있다. 깔끔하게 정리해야 한다.
# Homebrew Node.js가 설치되어 있는지 확인
$ brew list | grep node
node
node@18
# Homebrew Node.js 제거
$ brew uninstall --ignore-dependencies node
$ brew uninstall --ignore-dependencies node@18
# 혹시 남아있는 심볼릭 링크 확인
$ ls -la /opt/homebrew/bin/node 2>/dev/null
# 출력이 없으면 정상
# npm 글로벌 경로도 확인
$ which node
/Users/dev/.nvm/versions/node/v22.12.0/bin/node
$ which npm
/Users/dev/.nvm/versions/node/v22.12.0/bin/npm
# nvm 경로가 우선순위에 있는지 PATH 확인
$ echo $PATH | tr ':' '\n' | head -5
/Users/dev/.nvm/versions/node/v22.12.0/bin
/opt/homebrew/bin
/opt/homebrew/sbin
/usr/local/bin
/usr/bin
# nvm 관리 노드가 최상위에 있으면 정상입니다
# 글로벌 패키지 재설치 (필요한 것만)
$ npm install -g typescript ts-node nodemon
added 3 packages in 2s
중요: Homebrew로 설치한 다른 패키지 중 node에 의존하는 것이 있을 수 있다. brew uninstall 시 --ignore-dependencies 플래그를 사용하되, 이후 brew doctor로 문내가 없는지 확인하자.
nvm vs fnm vs Volta 비교
nvm이 사실상 표준이긴 하지만, 속도나 기능 면에서 대안이 존재한다. 상황에 따라 다른 선택이 나을 수 있다.
| 항목 | nvm | fnm | Volta |
|---|---|---|---|
| 언어 | Shell script | Rust | Rust |
| 쉘 시작 속도 | 느림 (~200ms) | 빠름 (~5ms) | 빠름 (~3ms) |
| .nvmrc 지원 | 네이티브 | 호환 | package.json 방식 |
| 자동 전환 | 쉘 훅 수동 설정 | 내장 (--use-on-cd) | 내장 (shim 방식) |
| Windows 지원 | nvm-windows (별도) | 지원 | 지원 |
| npm/yarn 버전 관리 | Node별 분리 | Node별 분리 | package.json 고정 |
| 생태계 점유율 | 압도적 1위 | 빠르게 성장 | 기업 환경 중심 |
# fnm 설치 (속도가 중요하다면)
$ brew install fnm
# ~/.zshrc에 추가
eval "$(fnm env --use-on-cd)"
# fnm 사용법은 nvm과 거의 동일
$ fnm install 20
$ fnm use 20
$ fnm default 20
# Volta 설치 (팀 단위 도구 버전 고정이 필요하다면)
$ curl https://get.volta.sh | bash
# Volta는 package.json에 직접 버전을 기록
$ volta pin node@20
# package.json에 아래가 추가됨:
# "volta": { "node": "20.18.1" }
결론적으로, 터미널 시작 속도가 체감될 정도로 느리다면 fnm으로 갈아타는 것도 좋은 선택이다. 다만 CI/CD 환경이나 팀 프로젝트에서는 .nvmrc가 사실상 표준이라, nvm과의 호환성이 보장되는 fnm이 실용적이다. Volta는 Node.js뿐 아니라 npm/yarn 버전까지 package.json에 고정하고 싶은 팀에 적합하다.
나는 개인 프로젝트에서는 fnm을 쓰다가, 결국 협업 환경과의 호환성 때문에 nvm으로 돌아왔다. 대부분의 오픈소스 프로젝트 README가 "nvm use"를 안내하고 있어서, 이 생태계를 거스르기가 어렵더라.