"npm, yarn, brew... 아무거나 쓰면 되는 줄 알았다"
11년간 QA 엔지니어로 일하면서 개발자들과 나눴던 대화 중 가장 기억에 남는 순간이 있습니다.
"빌드 에러가 나는데요, 'Cannot resolve module axios'라고..."
"package.json에 axios 있어요?"
"네, 있어요. 분명히 있는데..."
"yarn으로 설치했어요? npm으로 했어요?" "어... 그게 중요한가요?"
그때의 저는 정말 몰랐습니다.
'패키지 관리자가 달라도 package.json은 같은 거 아닌가? 왜 구분해서 물어보는 거지?'
QA 엔지니어로서 저는 패키지 관리자를 "개발자가 쓰라는 대로 쓰는 명령어" 정도로만 이해했습니다. README에 npm install이라고 적혀있으면 그대로, yarn이라고 적혀있으면 그대로. 하지만 왜 프로젝트마다 다른지, 무슨 차이가 있는지는 전혀 관심이 없었습니다.
JamesCompany 프로젝트를 직접 개발하면서, 이 무지함이 얼마나 큰 시간 낭비를 가져오는지 뼈저리게 깨달았습니다.
프로젝트를 시작하면서 가장 먼저 한 실수는 아무 생각 없이 패키지를 설치한 것이었습니다.
며칠 후 git status를 확인해보니,
"어? 두 개 다 있네? 뭐 상관없겠지."
이게 재앙의 시작이었습니다.
처음엔 lock 파일을 단순한 설치 기록 정도로 생각했습니다.
'package.json이 있으면 되는 거 아닌가?'
하지만 첫 배포에서 바로 문제가 터졌습니다.
로컬에서는 완벽하게 돌아가는데 배포만 실패했습니다. 이유를 찾는 데 하루가 걸렸습니다.
알고 보니,
react-router-dom을 npm으로 설치
npm은 package-lock.json에만 기록
Vercel은 yarn.lock을 우선시해서 yarn으로 설치
yarn.lock에는 react-router-dom이 없었음.
결국 모든 패키지를 yarn으로 다시 설치해야 했습니다.
이 경험으로 깨달은 것.
Lock 파일은 "정확히 어떤 버전을 설치할지"를 기록한 설계도입니다. 두 개의 설계도가 있으면 충돌은 필연입니다.
더 심각한 문제는 GitHub Actions에서 발생했습니다. 로컬에서 모든 테스트를 통과하고 PR을 올렸는데,
"아니, 방금 yarn install 했는데 왜 lock 파일을 업데이트하래?"
디버깅 끝에 발견한 원인
로컬: yarn 1.22.21
GitHub Actions: yarn 1.22.19
단 두 패치 버전 차이였지만, lock 파일 형식이 미묘하게 달랐습니다. 신버전 yarn이 생성한 lock 파일을 구버전이 읽으면서 문제가 발생했습니다.
해결책은 모든 환경의 yarn 버전을 통일하는 것이었습니다.
하지만 이것도 완벽하지 않았습니다.
나중에 협업하게 되면?
새로운 개발자가 온다면?
Frontend를 yarn으로 정리하고 나니, Backend Python은 또 다른 도전이었습니다. 처음엔 당연히 pip를 썼습니다.
$ pip install fastapi
그런데 requirements.txt를 만들어보니 문제가 보였습니다.
FastAPI 하나 설치했는데 47개 패키지가 나왔습니다. 뭐가 내가 설치한 거고 뭐가 의존성인지 구분이 안 됐습니다.
그때 동료 QA 엔지니어가 uv를 추천했습니다.
속도가 충격적이었습니다. pip로 30초 걸리던 설치가 0.5초에 끝났습니다. Rust로 만들어진 uv의 성능은 압도적이었습니다.
하지만 더 중요한 건 일관성이었습니다.
macOS 개발자로서 Homebrew는 필수였습니다.
시스템 도구들은 모두 brew로 설치했습니다.
그런데 Node.js도 brew로 설치하면서 문제가 시작됐습니다.
프로젝트마다 다른 Node 버전이 필요한데, brew는 시스템에 하나만 설치합니다.
결국 nvm을 추가로 설치했는데,
알고 보니 brew로 설치한 node가 PATH에서 우선순위가 높았습니다.
해결책은 brew의 node를 제거하고 nvm만 사용하는 것이었습니다.
이렇게 정리하니 깨달았습니다.
각 도구는 자신만의 영역이 있습니다.
Homebrew는 시스템 도구, nvm은 Node.js, yarn은 프로젝트 의존성.
개발 초기, 자주 쓰는 도구들을 전역으로 설치했습니다.
그런데 프로젝트마다 다른 버전이 필요했습니다. A 프로젝트는 TypeScript 4.9, B 프로젝트는 5.0을 요구했습니다.
전역 설치의 문제점은 다음과 같았습니다.
버전 충돌
프로젝트별 설정 불가
추후 협업 시 환경 불일치
해결책은 모든 것을 프로젝트 로컬에 설치하고 npx나 yarn으로 실행.
약 3개월 간 JamesCompany를 개발하면서 정립한 패키지 관리 원칙.
1. 도구별 역할 분리
2. 환경 통일을 위한 파일들
3. README에 명확한 가이드
예전의 저였다면, 이렇게 버그를 리포트했을 것입니다.
> "패키지 설치 에러 발생. 의존성 문제인 것 같습니다."
이런 경우, 저는 이렇게 리포트할 것 같습니다.
이제는 "의존성 문제"가 아니라 구체적인 원인과 해결책을 제시할 수 있습니다.
11년차 QA 엔지니어였던 저는 패키지 관리자를 "그냥 설치하는 명령어" 정도로 생각했습니다. 하지만 약 3개월 간 직접 개발하면서 깨달았습니다.
- yarn vs npm: 단순한 취향이 아니라 프로젝트의 일관성 문제
- lock 파일: 선택이 아닌 필수, 재현 가능한 환경의 핵심
- 버전 명시: "최신 버전"은 없다, 모든 것은 특정 버전이어야 함.
- 도구의 영역
(1) brew는 시스템
(2) nvm은 Node.js
(3) yarn은 패키지
가장 큰 깨달음은 "도구를 제대로 이해하는 데 쓴 시간은 나중에 디버깅 시간을 10배 이상 아껴준다"는 것이었습니다.
이제 새 프로젝트를 시작할 때 가장 먼저 하는 일
1. 패키지 관리자 결정 (yarn으로 통일)
2. 버전 고정 파일 생성 (.nvmrc, .python-version)
3. README에 명확한 설치 가이드 작성
4. CI/CD 환경 동일하게 설정
다음 편에서는 "데이터베이스 마이그레이션"에 대해 다루겠습니다.
"그냥 ALTER TABLE 하면 되는 거 아니야?"라고 생각했던 시절의 이야기...
"하나의 프로젝트엔 하나의 패키지 관리자.
이 단순한 원칙이 수많은 디버깅 시간을 아껴줍니다."