목차
들어가며
팀 프로젝트에서 “내 PC에서는 되는데?”라는 말을 들어본 적이 있으신가요? 이 문제의 가장 흔한 원인 중 하나가 바로 Node.js 버전 불일치입니다.
개발자마다 로컬에 설치된 Node.js 버전이 다르면, 의존성 설치 결과가 달라지거나, 빌드가 실패하거나, 런타임에 미묘한 버그가 발생할 수 있습니다. 특히 모노레포 환경에서는 여러 앱이 같은 Node.js 런타임을 공유하기 때문에, 버전 통일이 더욱 중요합니다.
이 글에서는 .nvmrc, package.json의 engines 필드, 그리고 Yarn Berry 커스텀 플러그인을 조합하여 로컬 개발 환경과 CI 환경 모두에서 Node.js 버전을 강제하는 방법을 소개합니다.
왜 Node.js 버전을 고정해야 하는가?
1. 보안 취약점 대응
Node.js는 주기적으로 보안 패치를 릴리스합니다. 특정 버전에서 발견된 취약점이 팀 내 일부 개발자의 환경에만 패치되어 있다면, 개발-스테이징-프로덕션 간의 동작이 달라질 수 있으며, 심각한 보안 위험이 발생할 수 있습니다. 모든 개발자가 동일한 버전을 사용하도록 강제하면, 보안 패치 적용이 누락되는 상황을 방지할 수 있습니다.
2. 버전에 따른 빌드 결과물 차이
같은 소스코드라도 Node.js 버전에 따라 빌드 결과물이 달라질 수 있습니다. 네이티브 모듈의 컴파일 결과, crypto 모듈의 동작, fs API의 변경 등이 대표적인 예입니다. 버전이 다르면, 개발자마다 다른 빌드 결과물이 나올 수 있고, 이는 디버깅을 어렵게 만듭니다.
3. 의존성 호환성
package-lock.json이나 yarn.lock이 같아도 Node.js 버전이 다르면 선택적 의존성(optional dependencies)이나 플랫폼별 패키지의 설치 결과가 달라질 수 있습니다. 예를 들어, Node.js 24에서는 fsevents가 설치되지만, Node.js 20에서는 설치되지 않을 수 있습니다. 이런 차이는 개발자마다 다른 의존성 트리를 만들어 낼 수 있으며, 예측 불가능한 버그로 이어질 수 있습니다.
버전 고정을 위한 3가지 레이어
하나의 방법에만 의존하지 않고, 여러 레이어를 조합하면 더 확실하게 버전을 강제할 수 있습니다.
레이어 1: .nvmrc — 버전의 단일 진실 공급원(Single Source of Truth)
.nvmrc 파일은 프로젝트 루트에 위치하며, 해당 프로젝트에서 사용할 Node.js 버전을 명시합니다.
24.13.0
개발자는 nvm use 명령어로 이 버전으로 전환할 수 있습니다:
$ nvm use
Found '/path/to/project/.nvmrc' with version <24.13.0>
Now using node v24.13.0 (npm v10.x.x)
Tip: 셸 설정(
.zshrc,.bashrc)에 자동 전환 스크립트를 추가하면, 프로젝트 디렉토리에 진입할 때 자동으로 올바른 Node.js 버전으로 전환됩니다.
nvm 자동 전환 스크립트 설정
매번 nvm use를 수동으로 입력하는 것은 번거롭고 잊기 쉽습니다. 셸 설정 파일에 다음 스크립트를 추가하면, cd로 디렉토리를 이동할 때 .nvmrc 파일을 감지하여 자동으로 Node.js 버전을 전환해 줍니다.
Bash (~/.bashrc):
cdnvm() {
command cd "$@" || return $?
nvm_path="$(nvm_find_up .nvmrc | command tr -d '\n')"
# .nvmrc 파일이 없는 디렉토리로 이동한 경우 기본 버전으로 복원
if [[ ! $nvm_path = *[^[:space:]]* ]]; then
declare default_version
default_version="$(nvm version default)"
if [[ $default_version == "N/A" ]]; then
nvm alias default node
default_version=$(nvm version default)
fi
if [[ $(nvm current) != "$default_version" ]]; then
nvm use default
fi
elif [[ -s "${nvm_path}/.nvmrc" && -r "${nvm_path}/.nvmrc" ]]; then
declare nvm_version
nvm_version=$(<"${nvm_path}/.nvmrc")
declare locally_resolved_nvm_version
locally_resolved_nvm_version=$(nvm ls --no-colors "$nvm_version" | command tail -1 | command tr -d '\->*' | command tr -d '[:space:]')
if [[ "$locally_resolved_nvm_version" == "N/A" ]]; then
nvm install "$nvm_version"
elif [[ $(nvm current) != "$locally_resolved_nvm_version" ]]; then
nvm use "$nvm_version"
fi
fi
}
alias cd='cdnvm'
cdnvm "$PWD" || exit
Zsh (~/.zshrc):
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
위 스크립트를 설정하면 다음과 같이 동작합니다:
.nvmrc가 있는 프로젝트 디렉토리로 이동하면 자동으로 해당 버전으로 전환됩니다.- 해당 버전이 설치되어 있지 않으면 자동으로
nvm install을 실행합니다. .nvmrc가 없는 디렉토리로 이동하면 nvm의 기본(default) 버전으로 복원됩니다.
하지만 .nvmrc만으로는 강제성이 없습니다. 자동 전환 스크립트를 설정하지 않은 개발자나, nvm use를 실행하지 않은 경우에는 여전히 다른 버전의 Node.js로 작업하게 됩니다.
레이어 2: package.json의 engines 필드
package.json에 engines 필드를 추가하여 허용되는 Node.js 버전을 선언할 수 있습니다.
{
"engines": {
"node": "24.13.0"
},
"engineStrict": true
}
npm을 사용하는 경우, 프로젝트 루트의 .npmrc 파일에 engine-strict 옵션을 설정하면 npm install 시 버전이 맞지 않을 때 에러가 발생합니다:
# .npmrc
engine-strict=true
이 설정이 없으면 engines 필드는 경고(warning)만 출력하고 설치가 진행됩니다. engine-strict=true를 설정해야 비로소 에러로 처리되어 설치가 중단됩니다.
참고:
package.json의engineStrict필드는 npm v3부터 deprecated 되었습니다. 대신.npmrc파일에engine-strict=true를 설정하는 것이 올바른 방법입니다.
하지만 Yarn Berry(v2 이상)에서는 engines 필드를 기본적으로 검증하지 않습니다. .npmrc의 설정도 Yarn Berry에는 적용되지 않습니다. 이것이 바로 다음 레이어가 필요한 이유입니다.
레이어 3: Yarn Berry 커스텀 플러그인 — 진짜 강제하기
Yarn Berry는 플러그인 시스템을 제공하며, validateProject 훅을 통해 yarn install 등의 명령 실행 전에 커스텀 검증 로직을 삽입할 수 있습니다.
플러그인 코드
.yarn/plugins/plugin-check-node.js:
module.exports = {
name: 'plugin-check-node',
factory: () => ({
hooks: {
validateProject(project) {
const fs = require('fs');
const path = require('path');
// Yarn Berry의 project.cwd는 PortablePath(POSIX 형식)를 반환합니다.
// Windows에서는 드라이브 레터 앞의 "/"를 제거해야 유효한 경로가 됩니다.
let cwd = project.cwd;
if (process.platform === 'win32' && /^\/[A-Za-z]:/.test(cwd)) {
cwd = cwd.slice(1);
}
const nvmrcPath = path.join(cwd, '.nvmrc');
let required;
try {
required = fs.readFileSync(nvmrcPath, 'utf8').trim();
} catch (error) {
if (error && error.code === 'ENOENT') {
throw new Error(
`\n\x1b[31mUnable to determine required Node version.\x1b[0m\n` +
`The .nvmrc file was not found at: ${nvmrcPath}\n\n` +
`Please create a .nvmrc file with the required Node version,\n` +
`or ensure you are running the command in the correct project directory.\n`
);
}
throw new Error(
`\n\x1b[31mUnable to read required Node version from .nvmrc.\x1b[0m\n` +
`Path: ${nvmrcPath}\n` +
`Underlying error: ${error && error.message ? error.message : String(error)}\n`
);
}
const current = process.versions.node;
if (current !== required) {
throw new Error(
`\n\x1b[31mNode version mismatch!\x1b[0m\n` +
`Required: ${required}\n` +
`Current: ${current}\n\n` +
`Please run: nvm use\n`
);
}
},
},
}),
};
플러그인이 하는 일
.nvmrc파일에서 필요한 Node.js 버전을 읽어옵니다.- 현재 실행 중인 Node.js 버전(
process.versions.node)과 비교합니다. - 버전이 일치하지 않으면 에러를 발생시켜 명령 실행을 중단합니다.
- Windows 환경에서의 경로 호환성도 처리합니다.
플러그인 등록
.yarnrc.yml에 플러그인을 등록합니다:
nodeLinker: node-modules
plugins:
- .yarn/plugins/plugin-check-node.js
동작 결과
올바르지 않은 Node.js 버전으로 yarn install을 실행하면 다음과 같은 에러가 출력됩니다:
Node version mismatch!
Required: 24.13.0
Current: 20.11.0
Please run: nvm use
이 에러는 yarn install 뿐만 아니라, Yarn을 통해 실행되는 모든 명령(yarn build, yarn dev 등)에서 발생합니다. 즉, 틀린 버전으로는 아무 작업도 할 수 없게 됩니다.
CI 환경에서의 버전 통일
로컬 개발 환경뿐만 아니라, CI(Continuous Integration) 환경에서도 동일한 Node.js 버전을 사용해야 합니다.
GitHub Actions에서는 actions/setup-node의 node-version-file 옵션을 통해 .nvmrc 파일을 직접 참조할 수 있습니다. .nvmrc에 기록된 버전(예: 24.13.0)을 setup-node 액션이 읽어 해당 버전의 Node.js를 자동으로 설치하므로, 워크플로우 파일에 버전을 하드코딩할 필요가 없습니다:
steps:
- name: Setup node
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
node-version-file 옵션은 .nvmrc 외에도 .node-version, .tool-versions 등의 파일 형식을 지원합니다. 중요한 점은 node-version이 아닌 node-version-file을 사용함으로써, 버전 번호를 워크플로우 YAML에 직접 작성하지 않는다는 것입니다. 이를 통해 .nvmrc 파일이 로컬 개발 환경과 CI 환경 양쪽의 단일 진실 공급원(Single Source of Truth) 역할을 하게 됩니다.
- 단일 진실 공급원(Single Source of Truth): 특정 데이터나 설정값을 오직 하나의 출처에서만 관리하는 원칙입니다. 여러 곳에 같은 값을 중복 작성하면 업데이트 시 누락이 발생하기 쉬운데, 단일 진실 공급원을 두면 한 곳만 수정해도 이를 참조하는 모든 곳에 자동으로 반영됩니다. 여기서는
.nvmrc파일이 Node.js 버전의 단일 진실 공급원 역할을 합니다.
실제 프로젝트에서는 이 설정을 재사용 가능한 Composite Action으로 분리하여, 모든 워크플로우에서 일관되게 사용할 수 있습니다.
# .github/actions/install_dependencies/action.yml
name: 'Install Dependencies'
runs:
using: 'composite'
steps:
- name: Setup node
uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- name: Enable Yarn
shell: bash
run: corepack enable
- name: Install dependencies
shell: bash
run: yarn install --frozen-lockfile
이렇게 하면 Node.js 버전을 업데이트할 때 .nvmrc 파일 하나만 수정하면 로컬과 CI 환경 모두에 자동으로 반영됩니다.
GitHub Actions에서의 워크플로우 설계에 대해 더 알고 싶다면, Jest 실행 Action 성능 개선하기나 모노레포에서 브랜치별 파일 변경 범위를 제한하는 GitHub Actions 워크플로우도 참고해 보세요.
버전 업데이트 프로세스
Node.js 버전을 업데이트해야 할 때(예: 보안 패치), 다음 파일들을 수정해야 합니다.
| 파일 | 역할 |
|---|---|
.nvmrc | 로컬 개발자 + CI의 Node.js 버전 지정 (단일 진실 공급원) |
package.json의 engines | npm 사용자를 위한 추가 안전장치 |
핵심은 .nvmrc 파일 하나만 수정하면 된다는 점입니다. 플러그인과 CI 설정은 모두 .nvmrc를 참조하므로, 버전 관리가 매우 간단해집니다.
전체 구조 요약
.nvmrc를 단일 진실 공급원으로 — 모든 도구가 이 파일을 참조하도록 설계합니다.- Yarn Berry 플러그인으로 강제성 확보 —
nvm use를 잊어도, 잘못된 버전으로는 작업이 불가능합니다. - CI에서도 동일한 소스 참조 —
node-version-file옵션으로.nvmrc를 직접 읽습니다. - Windows 호환성 고려 — 플러그인에서 Yarn Berry의 PortablePath를 네이티브 경로로 변환합니다.
- 버전 업데이트는 파일 하나만 —
.nvmrc수정 한 번으로 로컬과 CI가 모두 업데이트됩니다.
참고 자료
- Yarn Berry에서 Node 버전 고정하기 (GitHub Issue #1177)
- actions/setup-node — node-version-file 옵션
- Node.js 보안 릴리스 (2025년 12월)
제 블로그가 도움이 되셨나요? 하단의 댓글을 달아주시면 저에게 큰 힘이 됩니다!
앱 홍보
Deku가 개발한 앱을 한번 사용해보세요.Deku가 개발한 앱은 Flutter로 개발되었습니다.관심있으신 분들은 앱을 다운로드하여 사용해 주시면 정말 감사하겠습니다.