---
layout: post
title: "자잘한 도움말 #15 프로젝트별로 필요한 Node.js 버전이 다를 때"
subtitle: "작업 디렉토리가 바뀔 때마다 자동으로 Node.js 버전을 바꾸는 방법"
description: "버전 매니저, Version Manager, Node Version Manager, NVM, .nvmrc, Fast Node Manager, FNM, Volta, n, asdf, 쉘 커스텀 훅, Node.js, 자동으로 Node.js 버전 바꾸는 방법, nvm vs n vs asdf vs volta vs fnm"
date: 2024-04-23 16:50:00 +0900
categories: [Micro]
---

## TL;DR

- 한 개발 PC 내에 여러 Node.js 버전이 필요하다면 **버전 매니저(Version Manager)**를 설치하는 것이 좋다.
  - Node.js에선 **NVM**이 대표적인 버전 매니저다.  
  - NVM의 대체재로 **FNM**이라는 도구도 있다.

```sh
# fnm 설치
curl -fsSL https://fnm.vercel.app/install | bash

# 이 명령어로 환경 변수를 현재 터미널에 적용해야 fnm을 사용 가능하다.
eval "$(fnm env)"

# 설치할 수 있는 Node 버전들 확인
fnm ls-remote

# 특정 버전 설치
fnm install 16.16.0
```
- `.nvmrc` 파일을 사용해 프로젝트별 Node.js 버전을 명시할 수 있다.
  
```sh
# 현재 경로의 .nvmrc 파일에 명시된 버전의 Node 사용
fnm use
```

- `.bashrc` 파일 혹은 `.zshrc` 파일에 다음 명령어를 추가해 현재 터미널의 작업 디렉토리가 바뀔 때마다 자동으로 `fnm use` 명령어가 실행되도록 만들 수 있다.

```sh
# ~/.zshrc 또는 ~/.bashrc
eval "$(fnm env --use-on-cd)"
```

---

## 이하 그리 중요하진 않은 내용들

![일 많은 백수](https://i.postimg.cc/XqQQgQW3/temp-Image-PLY8-Np.avif)  
이상하다.. 나 백수인데..
{: .center .w-1-quarter .rounded-edge-16}

아무래도 나는 Node.js와는 떼려야 뗄 수 없는 사이가 되어버린 것 같다. 요즘 웹 프론트엔드 개발 하려면 대부분은 일단 Node.js 기반으로 프로젝트를 생성해야 하니깐. 나는 지금까지 여러 프로젝트를 진행해 왔는데, 그중에는 올해 초에 코드 작성을 시작한 것도 있고 몇 년 전에 시작되어서 아직도 운영되며 내 재정을 책임지는 서비스도 있다. 이렇듯 개발 PC에는 여러 프로젝트들이 공존할 수가 있다. 여기서 문제는 그 몇 년 전 프로젝트가 조금 까다로운 면이 있어서, 오래된 Node.js 버전을 요구한다는 점이다.

웬만하면 새로운 프로젝트에서는 최신 Node.js 버전을 쓰고 싶은데 옛날 프로젝트에 발목 잡히긴 싫어. [옛날 Node.js엔 `structuredClone()`도 없단 말야.](/micro/2023/12/10/micro-tip-13.html#tmi){:target="_blank"} 그럴 때 필요한 것이 이번 글이다. Node.js 버전을 관리하는 방법에 대해 알아보자.

### Node Version Manager

![nodejs install](https://i.postimg.cc/g2J3ZGxn/image.png)  
{: .center}

Node.js를 설치하려면 어떻게 해야 할까. 구글에 Node.js 설치 방법을 검색하면 Node.js 공식 사이트가 가장 먼저 노출된다. [공식 사이트의 다운로드 페이지](https://nodejs.org/en/download){:target="_blank"}에 들어가 OS에 맞는 최신 버전 인스톨러를 다운받아 실행하면 설치 끝. 인스톨러가 환경 변수도 알아서 등록해 줄 거고, 별문제 없이 쉘에서 `node` 명령어를 사용할 수 있게 된다. 특정 버전의 Node.js 하나만 설치하고자 한다면 이렇게 하면 된다. 하지만 앞서 말했듯이 지금 우리는 여러 Node.js 버전이 필요하다. 그냥 다른 버전의 인스톨러를 새로 설치하면 환경변수가 꼬인다거나 하지 않을까? 이런 문제를 해결하기 위해선 내 PC에 설치된 Node.js의 버전을 관리하는 별도의 프로그램이 필요하다. 그렇게 특정 도구의 버전을 관리하는 프로그램을 **버전 매니저(Version Manager)**라고 부른다. Node.js에서 가장 대표적인 버전 매니저가 바로 **Node Version Manager, NVM**이다.

![NVM](https://i.postimg.cc/cLKwhY3f/nvm-logo-color.png)  
{: .center}

NVM은 2010년 [Tim Caswell](https://twitter.com/creationix){:target="_blank"}에 의해 시작되어 현재는 Node.js에서 사실상 공식 취급을 받는 도구가 되었다. 아닌 게 아니라, 아까 말했던 공식 사이트 다운로드 페이지에서도 직접 [NVM 사용해 Node.js를 설치하는 방법](https://nodejs.org/en/download/package-manager){:target="_blank"}을 알려주고 있다.

```bash
# Linux/Mac 기준

# installs NVM (Node Version Manager)
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

# download and install Node.js
nvm install 20

# verifies the right Node.js version is in the environment
node -v # should print `v20.12.2`

# verifies the right NPM version is in the environment
npm -v # should print `10.5.0`
```

`curl`로 NVM을 설치한 다음 `nvm install 20` 명령어 한 줄이면 Node.js 20 버전이 설치된다. 자세한 사용법은 [NVM 리포지토리의 README](https://github.com/nvm-sh/nvm?tab=readme-ov-file#usage){:target="_blank"}에서 확인할 수 있다. 주로 사용하게 될 명령어는 다음과 같을 것이다:

```bash
# 설치할 수 있는 node 버전을 확인
nvm ls-remote

# 최신 버전 node를 설치
nvm install node

# 구체적인 버전의 node를 설치
nvm install 14.7.0

# 이 PC에서 특정 버전의 node를 사용하도록 설정
nvm use 14.7.0

node -v # should print `v14.7.0`
```

참 쉽죠?

### `.nvmrc`

이제 내 개발 PC에 여러 버전의 Node.js를 설치하는 문제는 해결됐다. 하지만 아직 문제가 남아있다. 예를 들어, 어제는 Node.js 최신 버전을 사용해 신규 프로젝트 A를 개발했는데 오늘은 5년 전에 개발한 웹 서비스 B를 유지보수해야 한다고 생각해 보자. NVM을 사용해 Node.js를 과거 버전으로 설정해야 하는데, 그 과거 버전이 정확하게 몇 버전인지 알 수 있어야 한다. 혹시 모르지 14 버전은 너무 오래돼서 안되고, 20 버전은 너무 최신이라 안 될지. 심지어 극단적인 사례로 v16.16.0에선 되고 v16.20.0에선 안될 수도 있다. **정말 그런 사례가 있는진 모르니까 너무 깊게 파고들진 말자.**{: .small} 이를 해결하기 위해서 우리는 프로젝트의 루트 디렉토리에 `.nvmrc`라는 파일을 만들어 이 프로젝트가 어떤 버전의 Node.js를 사용해야 하는지 명시할 수 있다.

```
v16.16.0
```

아무런 미사여구도 필요 없이 이 한 줄이면 `.nvmrc` 파일 끝. `v`는 붙여도 되고 안 붙여도 된다. 그리고 `>=18.16.0` 같은 형식으로 사용할 수 있는 버전의 범위를 지정할 수 있는지 물어보는 의견도 찾아볼 수 있었는데, [버전 매니저의 목적에 맞지 않다고 판단해 지원하지 않는다는 것 같다.](https://github.com/nvm-sh/nvm/issues/3167#issuecomment-1685397284){:target="_blank"}

아무튼, 아무 폴더 하나에 `.nvmrc` 파일을 만들어보자. 그런 다음 해당 경로에서 `nvm use`를 입력하면 NVM은 알아서 `.nvmrc` 파일을 읽고 Node.js의 버전을 변경해 준다. 이렇게 우리는 프로젝트별로 필요한 Node.js 버전이 다를 때 그 버전을 명시하고 관리하는 문제를 해결했다.

### 작업 디렉토리가 바뀔 때마다 자동으로 Node.js 버전 바꾸기

`.nvmrc` 파일이 있는 경로에서 `nvm use` 명령어를 입력하면 Node.js 버전이 바뀐다. 이 행위가 정말 쉽고 간단하고, **까먹기 좋다**. 자고로 개발자라면 이렇게 번거로운 반복 작업은 가만둘 수 없어야지. [스택 오버플로우에서 200개 이상의 upvote를 받은 이 글을 참고하자.](https://stackoverflow.com/questions/23556330/run-nvm-use-automatically-every-time-theres-a-nvmrc-file-on-the-directory){:target="_blank"} ([2차 출처는 여기](https://velog.io/@korany-lee/자동으로-nvm-use-해주는-script){:target="_blank"})

```sh
# ~/.zshrc
# nvm config
export NVM_DIR=~/.nvm
source $(brew --prefix nvm)/nvm.sh

# place this after nvm initialization!
autoload -U add-zsh-hook
load-nvmrc() {
  local node_version="$(nvm version)"
  local nvmrc_path="$(nvm_find_nvmrc)"

  if [ -n "$nvmrc_path" ]; then
    local nvmrc_node_version=$(nvm version "$(cat "${nvmrc_path}")")

    if [ "$nvmrc_node_version" = "N/A" ]; then
      nvm install
    elif [ "$nvmrc_node_version" != "$node_version" ]; then
      nvm use
    fi
  elif [ "$node_version" != "$(nvm version default)" ]; then
    echo "Reverting to nvm default version"
    nvm use default
  fi
}
add-zsh-hook chpwd load-nvmrc
load-nvmrc
```

```sh
# ~/.bashrc
_nvmrc_hook() {
  if [[ $PWD == $PREV_PWD ]]; then
    return
  fi
  
  PREV_PWD=$PWD
  [[ -f ".nvmrc" ]] && nvm use
}

if ! [[ "${PROMPT_COMMAND:-}" =~ _nvmrc_hook ]]; then
  PROMPT_COMMAND="_nvmrc_hook${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
fi
```

원리는 쉘의 CD(Change Directory) 명령어에 커스텀 훅을 추가하는 것이다. 본인이 사용하는 쉘 환경에 맞춰 위 스크립트를 각자의 .(~)shrc 파일에 추가해 주자. 쉘 터미널을 재실행한 다음 `.nvmrc`가 있는 폴더로 이동하면 자동으로 `nvm use`가 실행되는 것을 볼 수 있다.

### New Challenger

![tools](https://i.postimg.cc/Pr6wRy0p/temp-Image-G1-HK7v.avif)  
{: .center .rounded-edge-16 .w-3-quarter}

SW라는 분야 전반적으로 그렇겠지만 특히 웹 프론트엔드 개발자들은 더 변화에 민감하단 이미지가 있는 것 같다. 당장 나도 새 프로젝트마다 거의 항상 새로운 개발 도구를 시도한다. 패키지 관리자 NPM은 Yarn으로, 빌드 도구 CRA는 Vite로... 그럼 버전 관리자 NVM은 대체할 껀덕지가 어디 없을까?

![도전자들](https://i.postimg.cc/1Rc5HNTX/temp-Imagen-Jfys-E.avif)  
Here comes a new challenger!
{: .center .rounded-edge-16 .w-3-quarter}

공식 사이트에서도 소개하고 있고, 명칭도 "Node Version Manager"라서 더 da facto처럼 보이는 NVM. 그 아성에 도전하는 이들이 있다. n, asdf, Volta, FNM 등등... 이름 진짜 대충 지은 것 같은 물건도 보인다. 아무튼, 저마다 NVM보다 나은 점이 뭐라도 있기 때문에 등장했을 터. 간략하게 찾아본 각자의 특징은 다음과 같다.

**[n](https://github.com/tj/n){:target="_blank"}**  
**n**은 NVM의 비교적 초기 경쟁자였다. NPM을 사용해 설치한다는 점이 특징이다. 그래서 [이런 각 버전 매니저들 간의 다운로드 횟수 비교 페이지](https://npmtrends.com/fnm-vs-n-vs-nvm-vs-volta){:target="_blank"}에 n이 유독 압도적인 것으로 나오기도 한다. 다른 버전 매니저들은 NPM으로 설치할 일이 없으니까.

**[asdf](https://news.hada.io/topic?id=3690){:target="_blank"}**  
**asdf**는 여기서 소개할 다른 버전 매니저와 성격이 좀 다르다. Node.js의 버전만 관리하는 도구가 아니기 때문. 한 프로젝트에 Node.js 버전도 명시해야 하고, 동시에 Python 버전도 명시해야 하고 그러면 이 도구를 쓰면 된다. asdf는 `.tool-versions`라는 파일에 각 도구의 버전을 적어두는 방법으로 버전을 관리한다. 여러 도구를 관리해야 하므로 asdf는 플러그인 방식을 사용한다. Node.js 버전 관리가 필요하면 Node.js 플러그인을, Ruby 버전 관리가 필요하면 Ruby 플러그인을.

**[Volta](https://news.hada.io/topic?id=3001){:target="_blank"}**  
**Volta**는 빠르다. 이름부터 "나 빨라요." 싶은 뉘앙스를 풍기고 있다. Bash 스크립트로 작성된 NVM 그리고 n과 달리, Volta는 Rust로 작성되었다. C++의 대체제라는 그 Rust. Volta가 갖는 또 다른 특징은, `.nvmrc` 파일 대신 `package.json` 파일 내에서 다음과 같은 자체 속성을 사용하는 방식으로 프로젝트의 Node.js 버전을 관리한다는 점이다.  
```json
"volta": {
  "node": "16.16.0"
}
```

**[FNM(Fast Node Manager)](https://news.hada.io/topic?id=4678){:target="_blank"}**  
**FNM**도 빠르다. 이번엔 더 직설적으로 이름에서 빠름을 어필하고 있다. FNM이 보여주는 성능의 배경에도 역시 Rust가 존재한다. 요즘 나온 도구 중 빠르다는 건 다 Rust더라고. 아무튼 FNM은 Volta와 달리 사용자 입장에서 NVM과 비슷한 감각으로 사용할 수 있다. `.nvmrc` 파일을 그대로 사용할 수 있고, 제공하는 명령어도 비슷하다.

[Volta와 FNM의 자세한 비교에 대해선 이 글을 참고하길 추천한다](https://pozafly.github.io/node/node-version-manager/){:target="_blank"}. 각 버전 매니저가 내부적으로 어떻게 버전을 관리하는지까지 분석해 주신 좋은 글이다. 나는 `.nvmrc`를 그대로 적용할 수 있다는 점이 마음에 들어 새 버전 매니저로 FNM을 선택했다. 간단한 사용법을 알아보자.

```sh
# 설치
curl -fsSL https://fnm.vercel.app/install | bash

# 이 명령어로 환경 변수를 현재 터미널에 적용해야 fnm을 사용 가능하다.
eval "$(fnm env)"

# 설치 가능한 Node 버전들 확인
fnm ls-remote

# 특정 버전 설치
fnm install 16.16.0

# 현재 경로의 .nvmrc 파일에 명시된 버전의 Node 사용
fnm use
```

`eval "$(fnm env)"`가 조금 번거로울 텐데, 이 명령어의 진가는 따로 있다. 앞서 NVM에서 다뤘던 작업 디렉토리가 바뀔 때마다 Node.js 버전이 바뀌도록 만드는 방법을 FNM에도 적용하려면 `.zshrc` 파일에 이렇게 적어주면 된다.

```zsh
# ~/.zshrc 또는 ~/.bashrc

eval "$(fnm env --use-on-cd)"
```

이거 한 줄이면 같은 결과를 낸다. 아니 저 명령어가 뭐길래? `eval` 없이 그냥 `fnm env --use-on-cd` 명령어를 입력하면 다음과 같은 텍스트 출력이 나온다.

```zsh
export PATH="/Users/anteater/Library/Caches/fnm_multishells/31155_1713855355106/bin":$PATH
export FNM_DIR="/Users/anteater/Library/Application Support/fnm"
export FNM_MULTISHELL_PATH="/Users/anteater/Library/Caches/fnm_multishells/31155_1713855355106"
export FNM_VERSION_FILE_STRATEGY="local"
export FNM_COREPACK_ENABLED="false"
export FNM_NODE_DIST_MIRROR="https://nodejs.org/dist"
export FNM_LOGLEVEL="info"
export FNM_RESOLVE_ENGINES="false"
export FNM_ARCH="arm64"
autoload -U add-zsh-hook
_fnm_autoload_hook () {
    if [[ -f .node-version || -f .nvmrc ]]; then
    fnm use --silent-if-unchanged
fi

}

add-zsh-hook chpwd _fnm_autoload_hook \
    && _fnm_autoload_hook

rehash
```

위 `export` 부분은 FNM에게 필요한 환경 변수들, 그리고 그 아래 FNM이 만들어준 커스텀 훅이 있다. FNM이 이렇게 만들어준 명령어를 `eval` 명령어로 실행하는 것이다. Bash 환경에서 명령어를 실행하면 커스텀 훅 부분에서 다른 결과가 나올 것이다. 플랫폼에 따른 차이도 FNM이 직접 핸들링해 주고 있는 것. 여러모로 최신 도구답게 이렇게 사용자의 편의를 많이 봐주고 있다는 점을 알 수 있다.

## 내가 배운 것

- 특정 도구의 버전을 관리하는 도구, 버전 매니저.
- 쉘에도 훅이란 개념이 존재한다.
- 빠른 노드 버전 매니저, FNM.

이건 사소한 문제일 수 있는데, Node.js 환경이 이미 설치된 상태에서 NVM이나 FNM으로 다른 버전의 Node.js를 설치했다면 새 Node.js에선 기존에 글로벌로 설치했던 NPM 패키지들이 동작하지 않는다. 버전마다 분리된 전역 `node_modules/`를 가지기 때문에 글로벌 패키지들도 다시 설치해야 한다. 나는 `yarn`이 없는 명령어라고 나오길래 조금 당황했었다.