
한 줄 요약: npm install 또는 yarn install에서 버전 충돌/peer 오류가 터질 때, “왜” 생기는지 구조부터 이해하고, Node/패키지 매니저/의존 트리/호이스팅/lockfile 순서로 점검하면 90%가 즉시 해결된다. 이 글은 구글에 흩어진 팁들을 하나의 디버깅 루틴으로 묶어, 누구나 따라만 하면 해결되도록 만든 실전 가이드다.
0. 증상 미리 보기 (로그 판독법)
대표 에러 메시지
- ERESOLVE unable to resolve dependency tree
- peer dep missing: react@^18, required by some-lib@x.y.z
- Found: react@17 but peer required react@^18
- conflicting versions for... / dedupe failed / Package subpath './...' is not defined by "exports"
- Yarn Berry(PnP): YN0002: │ some-pkg provides... which doesn't satisfy what... requests
👉 핵심은 누가 누구의 버전을 요구하는지를 읽어내는 것. required by / depends on / peer 키워드를 표시해 두면 원인 노드가 보인다.
1. 먼저 개념을 맞추자: dependencies vs peerDependencies
- dependencies: 설치 시 트리 안에 직접 깔림. 소비자는 특별히 신경 안 써도 됨.
- peerDependencies: “나와 같은 레벨(루트 또는 상위)에 이 버전이 있어야 해”라는 호환 계약. 예: UI 라이브러리(플러그인)가 react를 peer로 요구.
- devDependencies: 빌드/테스트 도구 등 개발 시에만 필요.
npm 버전에 따른 차이 (중요)
- npm v6: peer 자동 설치 안 함 → 경고만.
- npm v7+: peer를 자동 해결하려고 시도 → 못 맞추면 ERESOLVE로 실패.
- yarn v1: npm v6와 유사(노드_modules hoisting). resolutions로 강제 정합 가능.
- yarn v2+ (Berry, PnP): 물리적 노드_modules 없음. 버전 불일치에 더 민감, 대신 정확한 제어 가능.
2. 최단 해결 루틴 (Check 1 → 7)
아래 순서로만 진행해도 대부분 해결된다. 각 단계는 빠르게 실패하고 다음 단계로 넘어가도록 설계했다.
✅ Check 1: Node & npm/yarn 버전 정합
- node -v, npm -v 또는 yarn -v 확인.
- . nvmrc/engines와 실제 런타임이 다른 경우가 잦다.
- 해결: nvm use 혹은 nvm install <version>으로 프로젝트 권장 Node로 맞춤.
✅ Check 2: Lockfile·캐시 초기화
- 단일 레포: rm -rf node_modules package-lock.json yarn.lock 후 재설치.
- 모노레포/워크스페이스: 루트와 패키지들 모두 지우고 루트에서 재설치.
- 캐시: npm cache verify / npm cache clean --force / yarn cache clean.
✅ Check 3: 최상위 peer 충족
- 로그에서 peer... required... 찾기 → 루트 package.json에 직접 설치.
- 예) react@18 요구 → npm i -S react@18 react-dom@18 (또는 yarn add react@18 react-dom@18).
- peer는 대개 루트에. 하위 패키지에만 깔아도 해결 안 됨.
✅ Check 4: 범위 맞추기(semver)
- ^18.2.0 vs ~18.2.0 vs 18.2.0 : 요구 범위가 좁으면 충돌.
- 해결: 가능한 한 요구 범위를 수용하는 상위 버전으로 올리되, 브레이킹 여부는 릴리스 노트로 체크.
✅ Check 5: dedupe/overrides/resolutions
- npm: npm dedupe로 트리 평탄화.
- npm: overrides로 하위 의존 버전 강제 (npm v8+).
{
"overrides": { "some-lib/react": "^18.2.0", "left-pad": "1.3.0" }
}
- yarn v1: resolutions 사용.
{
"resolutions": { "**/react": "18.2.0" }
}
- yarn Berry: packageExtensions / resolutions / constraints 조합.
✅ Check 6: 임시 회피 플래그 (주의)
- npm i --legacy-peer-deps (npm v7+의 peer 엄격 검사 비활성화)
- npm i --force (권장 X: 트리 깨짐/런타임 버그 위험)
- 배포 파이프라인에 상시 적용 금지. 로컬에서 원인 파악용으로만.
✅ Check 7: 대체 패키지/메이저 업그레이드
- 오래된 라이브러리(메인테넌스 중단)가 최신 React/TS와 peer 충돌.
- 포크/대체 패키지나 메이저 업그레이드 고려.
3. 상황별 실전 레시피
시나리오 A) React 17 프로젝트에 라이브러리가 react@^18 peer 요구
증상: Found: react@17.0.2 but peer required react@^18
해결 옵션
- 프로젝트 업그레이드: react@18로 올림(권장).
- 해당 라이브러리 다운그레이드: some-lib@<react17-compatible>.
- 임시 회피: --legacy-peer-deps (빌드/런타임 폭탄 가능, 장기 금지).
시나리오 B) Webpack/TS/ESLint 생태계 충돌
- ESLint 플러그인들이 서로 특정 ESLint/TypeScript 버전 peer 요구.
- 해결: 루트에 호환 매트릭스 맞춰 설치 → overrides/resolutions로 세부 패치 고정.
- 예)
{
"devDependencies": {
"eslint": "^9",
"@typescript-eslint/parser": "^8",
"@typescript-eslint/eslint-plugin": "^8"
},
"overrides": {
"@typescript-eslint/*": { "eslint": "^9" }
}
}
시나리오 C) 모노레포(Workspaces)에서 패키지 간 React 중복
- packages/a는 react@18, packages/b는 react@17.
- 해결: 워크스페이스 루트에서 단일 React 버전으로 합의, 각 패키지 peerDependencies에 react: ">=18 <19"처럼 범위 통일.
- Yarn Berry: constraints.pro로 일관성 강제.
시나리오 D) Yarn Berry(PnP)에서 PnP 규칙 충돌
- 에러가 엄격하게 터짐.
- 해결:. yarnrc.yml에 nodeLinker: node-modules로 임시 전환, 이후 점진적 PnP 적응 또는 packageExtensions로 누락된 peer 보강.
-
nodeLinker: node-modulespackageExtensions:"some-lib@*":peerDependencies:react: ">=18"
4. 도구 세트(Commands & 툴)
- 버전 인사이트: npx npm-why <pkg>, npx why <pkg>(yarn-why), npm ls <pkg>
- 업데이트 후보: npx npm-check-updates -u → npm i / yarn upgrade-interactive --latest
- 중복 제거: npm dedupe, yarn dedupe
- 호환 리포트: ESLint/TS/React 각 릴리스 노트, peer 매트릭스 문서화
- CI 재현: corepack enable로 Node별 npm/yarn 버전 고정, --frozen-lockfile/ci 모드 사용
5. 깔끔한 package.json 설계 패턴
- peer는 진짜 peer만: 플러그인/프레임워크가 공유해야 할 코어(react, next, eslint 등)만 peer로 선언.
- devDependencies로 개발 도구 격리: 앱 런타임에서 필요 없는 빌드 도구는 dev로.
- engines와 CI 동기화: "engines": { "node": ">=18 <21" } +. nvmrc.
- overrides/resolutions로 핫픽스 문서화: 임시 조치의 맥락을 주석/README에 기록.
- 모노레포는 루트 우선: 코어 버전(react, typescript 등)은 루트에만 설치, 각 패키지는 peer 범위만 명시.
6. 자주 묻는 질문(FAQ)
Q. --legacy-peer-deps를 항상 써도 되나요?
- A. 비추천. 설치는 되지만 런타임 불일치로 치명 버그가 숨어든다. 원인 해결 > 임시 플래그가 원칙.
Q. lockfile 지우는 게 항상 정답인가요?
- A. 팀/배포 환경에서 무분별 삭제는 재현성 깨짐. 문제 재현 후 PR 단위에서 신중히 갱신.
Q. React/TS 메이저 올리기 무서울 때?
- A. changesets로 점진 업그레이드, --filter로 모노레포 선택 설치, e2e 테스트 선 구축.
Q. Yarn에서 npm 전환(또는 반대) 해도 되나요?
- A. 가능하나 한 프로젝트에서 혼용은 금물. corepack으로 고정, 팀 컨벤션 명확히.
7. 한 장 요약 (체크리스트)
- Node 버전 맞췄나? (nvm use)
- Lockfile/캐시 초기화 했나?
- peer 요구 패키지를 루트에 정확 버전으로 설치했나?
- semver 범위 충족하나? (caret/tilde 확인)
- npm dedupe / overrides 또는 yarn resolutions 적용했나?
- 임시 플래그 남용하지 않았나?
- 모노레포면 루트-패키지 버전 합의했나?
8. 맺음말
의존성 충돌은 도구의 문제가 아니라 계약(compatibility contract)의 문제다. 이 글의 루틴대로 원인 → 범위 조정 → 구조 정리 → 도구 보조 순으로 접근하면, npm/yarn install 지옥에서 금방 빠져나올 수 있어요. 댓글로 구체 로그를 남겨주면, 실제 트리 기준으로 overrides/resolutions 스니펫까지 맞춤 제공해 보겠습니다.
