pnpm add eslint는 왜 최신 버전 10이 아니라 9를 설치했을까?
문제 상황
AI 번역 확장 프로그램을 만들면서, 린트를 설정하기 위해 eslint를 설치하였습니다. 저는 확장 프로그램 개발 프레임워크인 WXT를 사용하여 개발하고 있습니다.WXT에서는 템플릿 자체에서 eslint를 의존성을 두지 않고, 개발자가 원하면 설치할 수 있게 환경을 제공합니다. 아래는 당시 의존성을 설치하지 않았던 상태입니다.

저는 현재 타입스크립트를 사용하고 있었고, 타입스크립트 린팅이 필요했기 때문에, 아래 명령어로 의존성을 설치했습니다.
pnpm add -D eslint @eslint/js typescript-eslint저는 가장 최신 버전의 의존성이 설치될 것이라 기대했는데, 결과는 아래와 같았습니다.


뭔가 이상하지 않나요? eslint는 9 버전이 설치되었고, @eslint/js는 10 버전이 설치되었습니다. 이것이 문제가 되는 이유는 아래와 같이, @eslint/js의 peer dependency가 10버전 이상을 요구하고 있기 때문입니다.
pnpm peers check
일반적으로는, 최신 버전이 설치되면서 이러한 문제가 발생하지 않을텐데, 왜 이런 문제가 발생하게 된 것 일까요? 그건 제가 확장 프로그램과 웹사이트를 모노레포로 구성하였기 때문입니다.
문제가 발생한 이유
아래는 현재 제 레포의 폴더구조를 간략하게 나타낸 것입니다. 저는 pnpm workspace를 기반한 모노레포로 확장 프로그램과 확장 프로그램 소개 및 관련 문서와 관한 웹사이트를 각각의 앱으로 구성하고 있습니다. 웹사이트는 Next.js를 프레임워크로 사용하고 있습니다. 초기 상태는 extension에는 eslint가 설치되지 않았고, web에는 Next.js 템플릿을 사용하고 있었기 때문에, 설치된 상태였습니다.
opener/ # pnpm 워크스페이스 루트 (코드 없음, 설정만)
├── apps/
│ ├── extension/ # 실제 제품. 브라우저 확장 (WXT + React)
│ └── web/ # 확장 소개용 웹사이트 (Next.js 16 + Tailwind)
├── packages/ # 패키지
├── pnpm-workspace.yaml # apps/*, packages/* 를 워크스페이스로 등록
├── pnpm-lock.yaml # 두 앱이 공유하는 유일한 것
├── release-please-config.json # extension 만 등록
└── package.json # build/compile 을 pnpm -r 로 위임모노레포 환경에서 pnpm add 하게 되면, 워크스페이스에 이미 있는 버전을 재사용해서 사용합니다. 좀 더 자세한 과정을 살펴보면, 아래와 같은 과정을 거칩니다.
1️⃣ spec 결정이 이뤄집니다.
내가 버전을 명시하지 않았다면, 다른 워크스페이스 프로젝트의 package.json에서 그 패키지 선언을 찾아 spec을 가져와요. 여기서 spec이란 package.json에 적힌 의존성 속성 그대로를 말합니다. 위의 예시에서는 "eslint": "^9" 를 말합니다. 만약 어떤 워크스페이스 프로젝트에도 해당 의존성이 없다면 latest를 받아옵니다.
2️⃣ 버전 해석을 합니다.
그 spec을 만족하는 버전이 lockfile에 이미 있으면 그걸 사용합니다. 없으면 레지스트리에서 spec 안의 최신을 받습니다.
3️⃣ 설치한 해당 워크스페이스의 package.json에 기록합니다.
여기서 중요한 것은 spec 결정할 때 사용한 것을 그대로 복사하는 것이 아닌, 최종 설치된 버전으로 기반으로 쓴다는 게 좀 더 정확합니다. 다른 워크스페이스에서 "eslint": "^9" 이렇게 의존성이 명시되어 있으면, pnpm install은 아래와 같이 package.json을 기록하고 의존성을 설치합니다.
apps/web의package.json:"eslint": "^9"lockfile에 실제 적혀있는 버전:9.30.0
apps/extension에서 pnpm add -D eslint 한다면..pnpm add할 때 버전을 안 줬으니 옆 프로젝트를 살펴봅니다.web이^9를 쓰고 있으니. 캐럿(^) 범위라는 것을 인지합니다.^9를 만족하는 게lockfile에 이미 있나 확인합니다.9.30.0이 있으니 그걸 사용하는 것으로 결정합니다. 레지스트리에 더최신 버전인9.39.5가 있어도 다운 받지 않습니다.- 결과:
extension에서9.30.0를 사용하고,extension의package.json에는"eslint": "^9.30.0"이라고 의존성을 작성합니다.
그럼 lockfile로 버전이 고정되었는데, 이를 업데이트 하기 위해선 어떡해야하나요?
lockfile에 한번 버전이 위와 같이 명시되면, 바뀌지 않기 때문에, 업데이트를 수동으로 할 필요가 있을 수 있습니다. 범위 안에서 최신으로 올리고 싶을 때 아래 명령어를 사용합니다.
pnpm update eslint^9 범위 안에서 최신으로 올리고 lockfile을 갱신합니다. 10으로 넘어가진 않습니다. 메이저를 넘기려면 pnpm add eslint@10처럼 package.json의 범위 자체를 바꿔야 합니다.
이렇게, 기존에 설치된 패키지를 재사용하기 때문에, 위와 같은 문제가 발생한 것이었습니다. Next.js 템플릿 코드에서는 eslint 버전이 ^10 이 아니라 ^9로 명시되어 있었는데, 이때 9.39.5 이 버전이 설치되었고, lockfile에 명시되면서, app/extension의 package.json에 ^9.39.5로 명시되게 된 것입니다.
문제 해결
그러면 어떻게 해결해야할까요? 문제 상황 섹션에서 살펴본 것과 같이, 만약 @eslint/js를 10 버전으로 사용하기로 결정한다면, eslint 버전을 ^10 으로 올려줘야 합니다. 현재 해결책은 크게 세 가지 방향이 있습니다.
- 현재
app/extension에 설치된@eslint/js버전을^9으로 내린다. - 모노레포 전체의
eslint버전을^10으로 올린다. app/extension,app/web등 앱 별로 각자 맞는eslint버전을 사용하도록package.json에 별도로 명시한다.
각 해결 방법 중 어떤게 가장 적합할지 하나씩 살펴봅시다.
1️⃣ 현재 app/extension에 설치된 @eslint/js 버전을 ^9 으로 내린다
가장 간단한 해결책인 것 같지만, 그다지 좋은 선택은 아닙니다. 2026.09 기준 eslint의 9 버전대가 deprecated 되었기 때문입니다.

2️⃣ 모노레포 전체의 eslint 버전을 ^10 으로 올린다
이게 가능할까요? 한번 버전을 올려서 테스트해봅시다.

위 내용만 보았을때는 eslint-config-next 16.3.3의 peer 선언은 eslint >=9.0.0이라 10도 되는 것처럼 보이는데, 실제로 eslint 10 + eslint-config-next 16.3.3 + next로 구성한 프로젝트의 apps/web의 eslint.config.mjs를 돌려보면 아래와 같은 에러가 뜹니다.
TypeError: Error while loading rule 'react/display-name': contextOrFilename.getFilename is not a function이 이유는 eslint-config-next가 내부에서 쓰는 플러그인들이 eslint 10에서 제거된 API를 아직 쓰고 있어서 그렇습니다. eslint 10 릴리스 노트에 보면, context.getFilename 제거가 적혀 있고, 이에 따라 플러그인 호환을 확인하라고 써 있습니다.
프레임워크의 lint 프리셋 이슈 트래커를 살펴보면, 해당 내용이 vercel/next.js#89764와 eslint-plugin-react#4018에 이미 올라와 있는 것을 확인할 수 있습니다. eslint-config-next는 eslint-plugin-react에 의존하고 있고 eslint-plugin-react가 아직 eslint 10 을 지원하지 않기 때문에, 이러한 문제가 발생한 것입니다. 따라서 이 문제가 해결될 때까지 eslint를 버전 ^9로 고정하거나 settings.react.version을 명시해서 우회하라고 답변에 정리돼 있습니다.
따라서 Next.js 앱의 eslint 버전을 ^10 으로 올릴수 없게 되고, 최종적으로 모노레포 전체의 eslint 버전을 ^10 으로 올리긴 어렵습니다.
3️⃣ 별로 각자 맞는 eslint 버전을 사용하도록 package.json에 별도로 명시한다.
위와 같이 eslint의 버전을 통일시키기에는 어려워보입니다. 웹사이트와 확장 프로그램은 각각 별도로 배포되고 사용되는 애플리케이션에 가까우므로, 그냥 각각 가장 적합한 버전을 선택하는 것이 합리적이어 보입니다.
cd apps/extension && pnpm add -D eslint@latest @eslint/js typescript-eslint eslint-plugin-react-hooks
설치하고 한번 확인해보니, 문제가 없어보입니다.
🧐 패키지를 각 앱별로 구성하는데, 모노레포일 필요가 있을까?
현재는 패키지를 재사용하지도 않고, 공통으로 사용되는 패키지도 딱히 없어보입니다. 또한 웹사이트와 확장 프로그램은 각자 배포하는 곳도 달라 CI도 별도로 돕니다. 또한 버저닝도 확장 프로그램만 적용되도록 구성하였습니다. 또한 web 같은 사이트는 보통 버전을 안 매기고 push마다 배포만 하기때문에, 앞으로도 버저닝이 필요하지 않을 가능성이 큽니다.
그런데도 모노레포로 구성해야할 필요가 있을까요? 저는 아래 이유로 모노레포 구성을 유지하였습니다.
- 하나의 PR로 관련된 여러 패키지 및 앱들을 수정할 수 있습니다. 확장 동작을 바꾸면서 웹사이트 문서도 같은 커밋에서 고치는게 가능합니다.
- 공유 코드가 생긴다면 관리하기 쉽습니다. 확장 프로그램에서 네이티브 메시징 프로토콜을 사용할 예정인데, 만약 추가로 생긴 패키지들에서 타입을 공유하게 되면 모노레포에서는
packages/shared폴더 하나로 간단하게 관리할 수 있습니다. 레포가 둘이라면 npm에 publish하거나 git submodule을 써야 해서 관리 비용이 늘어날 수 있습니다. - 레포 거버넌스를 하나로 할 수 있습니다. 이미 만들어 둔 이슈 템플릿, PR 템플릿,
release-please설정 등을 하나로 관리할 수 있습니다.