mise로 nvm·pyenv·asdf 한 번에 대체하기: mise.toml, 자동 활성화, Tasks, 환경 변수
이 글의 핵심
mise-en-place(구 rtx)는 nvm·pyenv·rbenv·jenv·tfenv·asdf를 모두 대체하는 Rust 기반 통합 런타임·툴 버전 매니저입니다. .tool-versions 또는 mise.toml 한 파일로 프로젝트마다 Node·Python·Go·Ruby·Java·Terraform·kubectl 등을 고정하고, 디렉터리 진입 시 자동 활성화합니다. 이 글은 설치·사용·tasks 러너·환경 변수 관리·CI 통합을 실전 중심으로 다룹니다.
프로젝트마다 Node는 nvm, Python은 pyenv, Terraform은 tfenv로 관리하다 보면 셸 rc 파일에 초기화 스크립트가 줄줄이 쌓이고, 새 팀원이 합류할 때마다 “어떤 버전 매니저를 어떤 순서로 설치하라”는 문서가 길어집니다. mise는 이 역할을 하나의 바이너리와 하나의 설정 파일로 합칩니다. 이 글은 설치부터 프로젝트 설정, tasks, 환경 변수, CI까지 실제로 도입할 때 부딪히는 지점을 중심으로 정리합니다.
설치
macOS / Linux
curl https://mise.run | sh
echo 'eval "$(~/.local/bin/mise activate bash)"' >> ~/.bashrc # zsh/fish 각각
# 또는 brew
brew install mise
exec $SHELL -l
mise --version
Windows
winget install jdx.mise
# PowerShell
mise --version
첫 사용: Node + Python 설치
mise use --global node@20
mise use --global [email protected]
node -v # v20.x
python -V # 3.12.x
--global: 사용자 전체 기본값 (~/.config/mise/config.toml에 기록)- 프로젝트에서 오버라이드는
mise.toml또는.mise.toml(--global없이mise use를 실행하면 현재 디렉터리의 파일에 기록)
mise use는 설치와 설정 파일 기록을 한 번에 합니다. mise install node@20은 설치만 하고 어디에도 기록하지 않으므로, 팀원과 공유할 버전은 반드시 mise use로 파일에 남겨야 합니다. 버전을 "20"처럼 주 버전만 쓰면 그 시점의 최신 20.x가 설치되고, 시간이 지나 다른 사람이 설치하면 다른 패치 버전이 잡힐 수 있습니다. 재현성이 중요하다면 mise.lock 잠금 파일 기능을 쓰거나 "20.11.1"처럼 정확한 버전을 적는 편이 안전합니다.
프로젝트 설정: mise.toml
# mise.toml (프로젝트 루트)
[tools]
node = "20"
python = "3.12"
pnpm = "9"
go = "1.22"
terraform = "1.9"
kubectl = "1.30"
helm = "3.14"
[env]
NODE_ENV = "development"
DATABASE_URL = "postgres://localhost/myapp"
[tasks.dev]
run = "pnpm dev"
[tasks.build]
depends = ["lint", "test"]
run = "pnpm build"
[tasks.test]
run = "pnpm test"
[tasks.lint]
run = ["pnpm lint", "pnpm typecheck"]
디렉터리 진입만으로:
- 지정된 런타임 자동 활성화
- 환경 변수 자동 설정
mise run dev로 tasks 실행
단, 처음 클론한 저장소에서는 바로 동작하지 않을 수 있습니다. [env]나 tasks는 임의의 명령을 실행할 수 있기 때문에, mise는 처음 보는 설정 파일에 대해 “is not trusted” 에러를 내고 mise trust로 명시적으로 신뢰하도록 요구합니다. 악성 저장소를 cd만 해도 명령이 실행되는 사고를 막기 위한 장치이므로, 팀 문서의 온보딩 단계에 mise trust && mise install을 넣어 두면 첫날 질문이 줄어듭니다.
설정 파일은 디렉터리 계층을 따라 합쳐집니다. 현재 디렉터리에서 루트 방향으로 올라가며 찾은 mise.toml들과 글로벌 설정이 병합되고, 더 가까운 파일의 값이 우선합니다. 그래서 모노레포 루트에 공통 도구를 두고 하위 앱에서 일부만 덮어쓰는 구성이 가능합니다. 개인 설정은 mise.local.toml에 두고 .gitignore에 추가하면 팀 설정을 건드리지 않습니다.
asdf .tool-versions 호환
node 20.11.0
python 3.12.1
ruby 3.3.0
기존 asdf 프로젝트를 한 줄도 바꾸지 않고 mise로 사용 가능. 점진 이주가 쉽습니다. 팀 일부만 mise로 옮기는 동안에는 .tool-versions를 그대로 두고, 전원이 옮긴 뒤 mise.toml로 바꾸면서 [env], [tasks]를 추가하는 순서가 무난합니다. .tool-versions에는 환경 변수나 tasks를 넣을 수 없으므로, 그 기능이 필요해지는 시점이 전환 시점입니다.
도구 설치
mise install # 현재 디렉터리의 mise.toml/.tool-versions 전부 설치
mise install node@20
mise install node@latest
mise install --jobs=8 # 병렬 설치
Node LTS 별칭
mise use node@lts # 가장 최신 LTS
mise use node@lts-iron # LTS 코드네임
mise ls-remote node # 설치 가능한 버전 목록
활성화 전략
mise는 디렉터리 변경 감지 + 셸 훅으로 동작합니다.
cd ~/projects/app-a # node 20, python 3.12
node -v # 20.x
cd ~/projects/app-b # node 18, python 3.11
node -v # 18.x
mise activate가.mise.toml·.tool-versions를 탐색- 느린 호출은 거의 없음 (Rust + 캐시)
mise에는 두 가지 활성화 방식이 있고, 차이를 알아야 “터미널에서는 되는데 IDE에서는 안 된다”는 문제를 풀 수 있습니다. mise activate는 프롬프트가 뜰 때마다 훅을 실행해 PATH 자체를 바꿉니다. 명령 실행 시 중간 단계가 없어 빠르지만, 대화형 셸의 rc 파일을 거치지 않는 프로세스(VS Code의 디버거, GUI에서 실행한 IDE, cron)는 이 훅을 모릅니다. 반면 shim 방식(mise activate --shims 또는 ~/.local/share/mise/shims를 PATH에 추가)은 asdf처럼 node라는 작은 대리 실행 파일이 호출될 때마다 현재 디렉터리의 버전을 찾아 실행합니다. 약간의 오버헤드가 있지만 셸 훅 없이도 동작하므로, 대화형 셸에는 activate를, .zprofile이나 IDE 환경에는 shim을 두는 조합을 공식 문서도 안내합니다.
Tasks Runner
[tasks.ci]
run = [
"mise install",
"pnpm install --frozen-lockfile",
"pnpm lint",
"pnpm test",
"pnpm build",
]
[tasks.seed]
description = "DB 시드 데이터 로드"
run = "pnpm tsx scripts/seed.ts"
sources = ["scripts/seed.ts", "prisma/schema.prisma"]
outputs = ["seed.lock"]
[tasks.serve]
wait_for = ["seed"]
run = "pnpm dev"
depends: 의존 관계wait_for: 병렬 실행 + 완료 대기sources/outputs: make처럼 변경 감지로 불필요 실행 스킵
mise run ci
mise run serve
mise tasks # 전체 목록
별도 package.json scripts·Makefile·justfile 없이 모노레포 전체 tasks를 하나로 통합할 수 있습니다.
mise tasks의 가장 큰 장점은 task가 mise가 고정한 도구 버전과 [env] 환경에서 실행된다는 점입니다. Makefile에서 pnpm build를 부르면 그 셸 PATH에 어떤 pnpm이 있는지에 따라 결과가 달라지지만, mise run build는 [tools]에 적힌 버전으로 실행됩니다. 반대로 단점도 있습니다. 이미 package.json scripts에 익숙한 프론트엔드 팀에게 task 정의 위치가 두 곳이 되면 오히려 혼란스럽습니다. 저는 언어별 명령(pnpm test, pytest)은 각 생태계의 표준 위치에 두고, 여러 언어를 가로지르는 작업(CI 전체 실행, DB 시드, 인프라 배포)만 mise tasks로 올리는 쪽이 유지보수가 편했습니다.
환경 변수 / Secrets
[env]
NODE_ENV = "development"
APP_PORT = "3000"
# .env 파일 로드
_.file = ".env.local"
# 동적 값: exec 템플릿으로 셸 명령 결과 사용
GIT_BRANCH = "{{exec(command='git branch --show-current')}}"
# 1Password CLI 등 외부 비밀 저장소도 같은 방식으로 호출
OPENAI_API_KEY = "{{exec(command='op read op://Dev/OpenAI/api_key')}}"
direnv와 겹치는 영역을 mise가 흡수해 도구 수를 줄일 수 있습니다.
exec 템플릿은 편리하지만 디렉터리에 들어가거나 프롬프트가 뜰 때마다 평가될 수 있다는 점을 기억해야 합니다. 비밀 저장소 CLI가 매번 네트워크를 타거나 생체 인증을 요구하면 셸이 눈에 띄게 느려집니다. 자주 바뀌지 않는 값은 .env.local처럼 파일로 내려받아 _.file로 읽거나, 필요한 task에서만 불러오는 편이 낫습니다. 비밀 값은 커밋되는 mise.toml이 아니라 .gitignore된 mise.local.toml이나 sops로 암호화한 파일에 두는 것이 원칙입니다.
플러그인 / 백엔드
mise는 여러 백엔드(backend)를 지원:
- core: node·python·ruby·go·java·deno·bun·erlang 등 네이티브 구현
- asdf: 기존 asdf 플러그인 전부
- aqua: 수천 개 CLI 도구 (aquaproj.github.io)
- ubi: GitHub Release 바이너리 자동 설치
- npm: npm 글로벌 패키지
- cargo: Rust 크레이트
- pipx: Python 도구
- go:
go install
mise use aqua:hashicorp/[email protected]
mise use npm:prettier@3
mise use cargo:ripgrep@latest
mise use ubi:BurntSushi/ripgrep
같은 도구도 여러 백엔드로 설치할 수 있어서 어떤 것을 고를지가 실무적인 질문이 됩니다. 기준은 대체로 미리 빌드된 바이너리를 받는가, 서명·체크섬을 검증하는가, 설치에 다른 런타임이 필요한가입니다. aqua는 레지스트리에 체크섬 정보가 있어 검증이 되고 바이너리만 받으므로 CLI 도구에 적합합니다. cargo: 백엔드는 로컬에서 소스를 컴파일하므로 CI에서 수 분씩 걸릴 수 있고, npm:과 pipx:는 각각 Node와 Python이 먼저 설치되어 있어야 합니다. asdf 플러그인은 호환성이 넓지만 셸 스크립트를 그대로 실행하므로 가능하면 core나 aqua를 먼저 찾아보는 것이 좋습니다.
실전 예시: 멀티 언어 모노레포
repo/
mise.toml # 루트 공통 (node, python, go, terraform)
apps/
web/.mise.toml # node 20.11, pnpm 9.5
api/.mise.toml # python 3.12
worker/.mise.toml # go 1.22
infra/.mise.toml # terraform 1.9, kubectl 1.30
각 디렉터리 진입 시 해당 도구 버전으로 전환되어 git worktree·여러 터미널·VS Code 다중 워크스페이스 모두 자연스럽게.
CI 통합
GitHub Actions
- uses: jdx/mise-action@v2
with:
install: true
cache: true
- run: mise run ci
mise.toml의 [tools] + [tasks.ci]가 그대로 CI에서 실행되므로 로컬과 CI의 도구 버전이 일치합니다. 다만 OS 패키지, 시스템 라이브러리, 아키텍처(Apple Silicon과 x86 러너)까지 맞춰 주지는 않으므로 “완전히 같은 환경”은 아닙니다. 버전을 주 버전으로만 적었다면 CI 캐시가 비는 순간 새 패치 버전이 설치될 수 있으니, CI에서 재현성이 중요하면 정확한 버전이나 잠금 파일을 쓰는 것이 좋습니다.
GitLab CI
image: jdxcode/mise:latest
before_script:
- mise install
script:
- mise run test
- mise run build
셸 성능 최적화
# ~/.config/mise/config.toml (글로벌)
[settings]
idiomatic_version_file_enable_tools = ["node", "python"]
jobs = 8
idiomatic_version_file_enable_tools:.nvmrc·.python-version같은 도구별 버전 파일도 인식 (예전 이름은legacy_version_file)jobs: 병렬 설치 수raw: 설치 출력을 가공하지 않고 그대로 표시 (병렬 설치가 꺼짐)
Node 전용 기능
[tools]
node = "20"
[env]
_.path = ["./node_modules/.bin"] # 자동 PATH 추가
pnpm·yarn·bun은 독립 도구로 관리 (pnpm = "9")package.json의packageManager필드로 corepack을 쓰는 프로젝트라면, mise와 corepack 중 한 곳에서만 pnpm 버전을 정해야 충돌이 없습니다
_.path에 ./node_modules/.bin을 넣으면 npx 없이 로컬 설치 도구를 바로 실행할 수 있지만, 상대 경로라 그 디렉터리를 신뢰한다는 전제가 필요합니다. 의존성 중 하나가 ls나 git 같은 이름의 바이너리를 넣으면 시스템 명령을 가릴 수 있으므로, 신뢰하는 프로젝트에서만 쓰는 편이 안전합니다.
업데이트·관리
mise self-update
mise upgrade # 모든 툴 최신으로
mise outdated # 업데이트 가능 목록
mise prune # 미사용 버전 제거
mise cache clear # 캐시 정리
트러블슈팅
command not found: node
mise activate가 셸 rc에 설정됐는지exec $SHELL -l로 셸 재시작mise doctor로 진단
PATH에 다른 버전이 먼저 있음
/usr/local/bin/node·brew node 등 → 제거 또는 PATH 순서 조정mise which node로 어느 경로가 사용 중인지 확인
nvm에서 옮겨 온 경우라면 .zshrc에 남아 있는 nvm.sh 로딩 코드가 가장 흔한 원인입니다. nvm도 PATH를 조작하므로 둘을 같이 두면 어느 쪽이 나중에 실행됐는지에 따라 버전이 바뀝니다. 이전 매니저의 초기화 코드는 주석 처리하고 새 셸에서 확인하세요.
설치 속도 느림
mise settings set jobs 8처럼 병렬 설치 수를 늘림cargo:백엔드처럼 소스를 빌드하는 백엔드 대신 바이너리를 받는 aqua·ubi 백엔드로 교체
Python 빌드 실패
- mise는 기본적으로 미리 빌드된 Python 바이너리를 받으므로 보통은 빌드가 필요 없습니다.
python.compile설정으로 소스 빌드를 켰거나 미리 빌드된 바이너리가 없는 플랫폼일 때 빌드 의존성이 필요합니다 - macOS:
xcode-select --install, openssl/xz 등 brew 설치 필요 - Linux:
libffi-dev,libsqlite3-dev등
레거시 .nvmrc와 충돌
- mise는 기본적으로
.tool-versions·.mise.toml우선 idiomatic_version_file_enable_tools에node추가 시.nvmrc인식
비교 요약
| 도구 | 언어 범위 | 속도 | 설정 파일 |
|---|---|---|---|
| nvm | Node | 느림 | .nvmrc |
| fnm | Node | 빠름 | .node-version |
| Volta | Node + JS tools | 빠름 | package.json |
| pyenv | Python | 보통 | .python-version |
| asdf | 다수 | 느림 | .tool-versions |
| mise | 다수 + CLI 도구 | 빠름 | mise.toml / .tool-versions |
표의 속도는 셸 시작과 명령 실행 시 체감되는 오버헤드 기준입니다. nvm은 셸을 열 때마다 nvm.sh를 읽어 시작이 느린 것으로 유명하고, fnm·mise는 네이티브 바이너리라 이 부담이 거의 없습니다. 반대로 Volta는 package.json에 버전을 고정해 Node 프로젝트에서는 가장 자연스럽지만 Node 생태계 밖은 다루지 않습니다. 한 언어만 쓰는 팀이라면 그 언어 전용 도구로도 충분하고, 여러 런타임과 CLI 도구를 함께 고정해야 할 때 mise의 장점이 커집니다.
마무리
mise는 “도구마다 다른 버전 매니저를 쓰는” 불편을 하나의 바이너리와 asdf 호환성으로 해결하는 도구입니다. 설정은 mise.toml 하나로 모이고, 셸 활성화가 빠르며, CI에서도 같은 도구 버전을 재현할 수 있습니다. 도입할 때는 mise trust 온보딩, 기존 매니저 초기화 코드 제거, IDE용 shim 설정, 비밀 값 분리 네 가지를 먼저 챙기면 대부분의 초기 문제를 피할 수 있습니다. nvm·pyenv·rbenv·tfenv 중 둘 이상을 함께 쓰고 있다면 한 프로젝트에서 먼저 시험해 볼 만합니다.
자주 묻는 질문 (FAQ)
Q. mise를 설치했는데 예전 node 버전이 계속 잡히면 어떻게 하나요?
A. /usr/local/bin/node나 brew로 설치한 node가 PATH에서 먼저 잡히는 경우가 대부분입니다. mise which node로 실제 사용 중인 경로를 확인한 뒤 기존 설치를 제거하거나 PATH 순서를 조정합니다. command not found가 난다면 셸 rc에 mise activate가 들어갔는지 보고, mise doctor로 진단하는 것이 빠릅니다.
같이 보면 좋은 글
- Rye & uv — Python 패키징을 Rust 속도로, pip·poetry·pyenv 대체
- macOS 개발 환경 설정 가이드 | 필수 도구와 최적화
- Node.js 배포 가이드 | PM2, Docker, AWS, Nginx