3 분 소요

방금 전에는 됐는데?

[ AI_Todo 프로젝트 개발기 - DevOps #3] 환경설정 통일 Docker가 아닌 MIse를 선탁한 이유

1. 마침내 마주한 최종 보스: 런타임 환경

지난 글에서, 저는 WSL(Windows Subsystem for Linux) 환경을 통해 Spring 컴파일 시간을 절반으로 줄이는 극적인 성능 향상을 경험했습니다. 환호도 잠시, 곧이어 절망적인 에러 메시지가 터미널을 뒤덮었습니다…

pnpm: command not found
husky - pre-commit hook failed (code 127)

애써 구축했던 품질 파이프라인의 문지기, Husky가 완전히 동작 불능이 된 것…
원인은 명확했습니다. WindowsGit 이 실행하는 셸과 WSL 의 셸은 서로 다른 세상이었고, 각자의 PATH 환경 변수를 가졌는데 요약하자면 한쪽 세상에 설치된 pnpm을 다른 쪽 세상에서는 찾지 못하는, 전형적인 환경 불일치 문제 였습니다.

pnpm + Turborepo로 빌드 속도를 잡기 -> OK
ESLint + Husky로 코드 품질 잡기 -> OK

이제 마지막으로 이 모든 것을 안정적으로 실행시킬 환경 자체를 통일해야 하는, DevOps의 ‘최종 보스’와 마주하게 되었습니다.

2. 가장 강력한 무기, Docker를 쓰지 않은 이유

이 문제의 가장 확실하고 강력한 해결책은 Docker 라고 생각했습니다. OS 레벨까지 통째로 가상화하여 모든 개발자에게 100% 동일한 환경을 제공할 수 있고 저 역시 가장 먼저 Docker Dev Container 를 고려했습니다.
( 사실 아는 게 이거밖에 없었습니다. )

하지만 해결하려는 문제는 “Node.js와 pnpm의 버전 및 PATH 불일치” 인데 매번 이 문제를 해결하기 위해 OS 전체를 컨테이너를 뛰우는 것은 너무 귀찮다고 생각했습니다.

분명 세상에는 저보다 똑똑한 개발자들이 너무나 많기 때문에 이런 상황에 적합한 솔루션이 있다고 생각했고 정보를 찾아봤습니다.

그러던 중 개발스터디 모임에서 예전에 Mise 가 언급이 되었었는데 이게 갑자기 떠올랐었고 해당 도구와 Docker랑 한 번 비교했습니다.

관점 Docker Dev Container Mise (이번에 선택한 도구)
문제 해결 범위 OS, 라이브러리, 런타임 등 환경 전체 Node.js, pnpm 등 런타임 버전/PATH
자원 사용량 CPU/RAM 상시 점유 (높음) 호스트 직접 실행 (오버헤드 최소)
시작 속도 컨테이너 빌드 → 수 분 네이티브 캐시 → 수 초
IDE 통합 별도 플러그인 및 설정 필요 기존 IDE 설정 그대로 사용
개발자 경험(DX) 컨테이너 셸에 대한 학습 곡선 기존 셸 경험 유지, 명령어 몇 개만 추가

결론

Docker 는 훌륭한 도구지만, 지금 상황에서는 좀 과하기도하고 귀찮기도 했습니다.
왜냐하면 제가 필요한건 네이티브 환경 속도 그리고 ‘런타임 버전’이라는 핵심 문제만 해결 되면 충분한데, 도커컴포즈로 변경사항이 있을 때마다 빌드를 다시 하는 건 오히려 비용이 더 든다고 생각했기 때문입니다.
물론 도커에서도 이미지를 만들 때 캐싱을 할 수 있지만 레이어의 변경부분이 초반 부분이라면 뒤에 변경된 부분이 없는 레이어도 전부 빌드를 다시 해야하기 때문에 여러모로 Mise 가 낫다고 생각했습니다.

3. Mise 도입: 3단계 환경 표준화

mise를 통해 엉망이 된 런타임 환경을 바로잡는 과정은 간단했습니다.

1단계: .tool-versions로 런타임 명세화

프로젝트의 루트에 모든 환경이 따라야 할 단일 진실 공급원(SSoT)을 만들고 이 파일 하나면 충분!

# .tool-versions
# 이 프로젝트는 반드시 아래 버전의 도구를 사용해야 한다.
nodejs 22.17.0
pnpm   10.12.4

2단계: 깨진 Husky 훅 되살리기

mise의 가장 강력한 기능인 mise exec를 사용하여, 셸 환경에 구애받지 않는 pre-commit 훅을 만들기!

# .husky/pre-commit
#!/usr/bin/env sh
set -e

# mise가 없다면 경고 후 스킵 (온보딩 편의성)
if ! command -v mise >/dev/null 2>&1; then
  echo "⚠️ mise not found. Skipping pre-commit hooks."
  exit 0
fi

# 핵심: 현재 셸의 PATH를 무시하고,
# .tool-versions에 명시된 버전의 pnpm을 찾아 실행한다.
mise exec -- pnpm exec lint-staged

mise exec -- <command>는 현재 셸의 상태와 무관하게, .tool-versions에 정의된 정확한 버전의 도구가 포함된 임시 환경을 만들어 <command>를 실행 합니다.

이 덕분에 Windows Git Bash , WSL , macOS , CI 등 어떤 환경에서도 pre-commit 훅은 100% 동일하게 동작하게 만들 수 있었습니다.
( 물론 OS 차이에서 발생하는 미묘한 차이 ( 파일경로, OS 명령어등 )등을 100% 통제할 수는 없지만 현재 개발환경 셋팅 상황에서는 아주 적당 )

3단계: CI 파이프라인과 완벽한 동기화

로컬에서의 경험을 CI에 그대로 이식 -> jdx/mise-action은 이 모든 과정을 단 한 줄로 처리!

# .github/workflows/ci.yml
steps:
  - uses: actions/checkout@v4
  # mise 설치, .tool-versions 기반 런타임 설치, 캐싱까지 한번에 처리
  - uses: jdx/mise-action@v2
  - run: pnpm install --frozen-lockfile
  - run: pnpm run test

4. 최종 교훈: 무거운 문제와 가벼운 문제

이 3부작의 여정은 복잡성을 단계적으로 정복해나가는 과정이었습니다.

  1. 빌드 복잡성pnpm + Turborepo로 해결
  2. 코드 품질ESLint + Husky로 자동화
  3. 환경 불일치는 마침내 mise로 통일

모든 여정이 끝나고 얻은 가장 큰 교훈은 이것입니다.
문제의 무게에 맞는 도구를 선택해야 한다는 것.

Docker 는 여러 서비스와 데이터베이스까지 포함된 복잡한 ‘시스템’을 통째로 복제하는 무거운 문제에 가장 효과적인 망치라고 생각합니다. 하지만 제가 마주했던 ‘런타임 버전 불일치’는 상대적으로 가벼운 문제였고, 이 문제에는 mise라는 정밀 드라이버가 더 빠르고, 더 효율적이었습니다.

이제 저는 어떤 OS를 쓰든, 어떤 동료와 협업하든, git clonemise install 단 한 번이면 모든 준비가 끝납니다.
길고 길었던 환경 설정과의 싸움 끝에, 마침내 진짜 중요한 ‘개발’에만 집중할 수 있는 평화를 얻었습니다….

“방금 전에는 됐는데?” 라는 말은, 적어도 제 프로젝트에서는 이제 과거의 유물이 되었습니다.

환경 통합의 매직, Mise.
Very Good

댓글남기기