이 글에서 정리하는 내용
pnpm install 오류는 단순히 node_modules가 꼬인 문제로 끝나지 않는 경우가 많습니다. 특히 모노레포나 workspace 구조에서는 package.json, pnpm-lock.yaml, pnpm-workspace.yaml, CI의 frozen-lockfile 조건이 서로 맞는지 순서대로 봐야 원인을 놓치지 않습니다.
- 오류 문구별로 먼저 의심할 부분
- lockfile을 지우기 전에 확인할 명령어
- lockfile과 workspace 오류가 생기는 이유
- 로컬, CI, Docker에서 점검하는 순서
- 기본 점검 후에도 실패할 때 볼 케이스
- 다음 작업에서 남겨둘 체크리스트
오류 문구별로 먼저 의심할 부분

pnpm install이 실패하면 먼저 에러 문구를 분리해서 봐야 합니다. 같은 설치 오류처럼 보여도 원인은 서로 다릅니다. 로컬에서는 설치가 되는데 GitHub Actions, Vercel, Docker 빌드에서만 실패한다면 lockfile이 현재 package.json과 맞지 않는 상황일 가능성이 큽니다. 반대로 특정 내부 패키지를 찾지 못한다는 메시지가 나오면 workspace 범위나 패키지 이름부터 확인하는 것이 빠릅니다.
ERR_PNPM_OUTDATED_LOCKFILE이 보이면 package.json의 의존성 변경이 pnpm-lock.yaml에 반영되지 않았는지 봅니다. --frozen-lockfile 조건에서 멈춘다면 CI가 lockfile 수정을 허용하지 않는 상태로 설치를 실행하고 있는 것입니다. workspace:*나 workspace:^로 연결한 내부 패키지를 찾지 못한다면, 그 패키지가 실제 workspace 안에 포함되어 있는지와 package.json의 name이 일치하는지를 같이 봐야 합니다.
이때 바로 pnpm-lock.yaml을 삭제하고 다시 설치하면 당장은 통과할 수 있습니다. 하지만 팀 작업이나 배포 환경에서는 그 방식이 원인 추적을 어렵게 만듭니다. lockfile은 현재 프로젝트가 어떤 의존성 조합으로 설치되는지 고정하는 파일입니다. 삭제 후 재생성하면 직접 바꾼 패키지 외의 하위 의존성까지 같이 바뀔 수 있고, 코드 리뷰에서 어떤 변경이 실제 원인인지 구분하기 어려워집니다.
- CI에서만 실패하면
package.json과pnpm-lock.yaml의 불일치를 먼저 확인합니다. - workspace 패키지를 못 찾으면
pnpm-workspace.yaml의 packages 범위와 각 패키지의name을 확인합니다. - 사람마다 lockfile 변경이 반복되면 Node, pnpm 버전 고정 여부를 확인합니다.
- 삭제와 재설치는 원인 확인 후 마지막 단계로 둡니다.
lockfile을 지우기 전에 확인할 명령어
가장 먼저 할 일은 현재 상태를 남기는 것입니다. 설치 오류가 난 상태에서 바로 파일을 지우면 비교 기준이 사라집니다. 아래 명령으로 Node와 pnpm 버전, 변경 파일, frozen-lockfile 조건에서의 재현 여부를 확인합니다.
node -v
pnpm -v
git status --short
git diff -- package.json pnpm-lock.yaml pnpm-workspace.yaml
pnpm install --frozen-lockfile
package.json은 바뀌었는데 pnpm-lock.yaml이 바뀌지 않았다면, 의존성을 추가하거나 버전을 바꾼 뒤 lockfile을 갱신하지 않은 상태일 수 있습니다. 로컬에서 설치를 다시 수행하고 lockfile 변경분까지 함께 커밋해야 CI에서도 같은 의존성 그래프를 기준으로 설치됩니다.
pnpm install
git status --short
git add package.json pnpm-lock.yaml pnpm-workspace.yaml
git commit -m "fix: update pnpm lockfile"
node_modules가 꼬였다고 느껴질 때도 lockfile부터 삭제하지 않습니다. 먼저 lockfile만 package.json 기준으로 갱신되는지 확인하면 변경 범위를 작게 볼 수 있습니다. 설치 산출물 전체를 지우기 전에 잠금 파일의 변화만 분리해서 보면, 새로 생긴 차이가 의존성 변경 때문인지 설치 캐시 때문인지 구분하기 쉽습니다.
pnpm install --lockfile-only
git diff -- pnpm-lock.yaml
프로젝트에서 사용하는 pnpm 버전도 고정해두는 편이 낫습니다. 같은 lockfile을 두고도 pnpm major 버전이 다르면 lockfile 호환성이나 설치 검증 방식에서 차이가 날 수 있습니다. 아래 버전 숫자는 예시이며, 실제 프로젝트에서는 팀과 CI가 사용하는 값을 맞춰야 합니다.
{
"name": "workspace-install-demo",
"private": true,
"packageManager": "pnpm@11.1.0",
"scripts": {
"dev": "next dev",
"build": "next build",
"check:install": "pnpm install --frozen-lockfile"
}
}
workspace 의존성 오류라면 루트의 workspace 범위와 내부 패키지 이름을 같이 봐야 합니다. pnpm-workspace.yaml에 포함되지 않은 폴더는 workspace 패키지로 인식되지 않습니다. 또한 사용하는 쪽의 dependency 이름은 폴더명이 아니라 대상 패키지의 package.json name과 맞아야 합니다.
packages:
- "apps/*"
- "packages/*"
{
"name": "@site/admin",
"dependencies": {
"@site/ui": "workspace:*"
}
}
{
"name": "@site/ui",
"version": "1.0.0"
}
위 예시에서 @site/admin은 @site/ui를 workspace 패키지로 가져옵니다. 그런데 실제 UI 패키지의 이름이 @site/design-system이라면 설치는 실패합니다. 모노레포에서 폴더명을 ui로 둔 것과 패키지명이 @site/ui인 것은 다른 문제입니다.
lockfile과 workspace 오류가 생기는 이유
pnpm은 npm이나 yarn과 비슷하게 보이지만, workspace와 lockfile을 다루는 기준이 꽤 엄격합니다. 특히 CI 환경에서는 lockfile을 마음대로 갱신하지 않는 조건으로 설치되는 경우가 많습니다. 로컬의 pnpm install은 lockfile을 새로 수정하면서 통과했지만, 배포 환경에서는 같은 변경을 허용하지 않아 실패할 수 있습니다.
이 동작은 번거로워 보여도 배포 재현성을 위해 필요합니다. 배포 서버가 설치할 때마다 lockfile을 새로 바꾼다면, 오늘 통과한 빌드와 내일 통과한 빌드가 다른 의존성 조합을 가질 수 있습니다. 프로젝트가 작을 때는 잘 드러나지 않지만, 여러 앱과 공용 패키지를 묶은 모노레포에서는 이런 차이가 빠르게 커집니다.
workspace 오류는 조금 다른 지점에서 생깁니다. workspace: 프로토콜은 “현재 workspace 안에 있는 패키지를 사용하라”는 의미입니다. 그래서 조건에 맞는 로컬 workspace 패키지가 없으면 외부 registry 패키지로 조용히 대체하지 않고 설치를 실패시킵니다. 이 실패는 불편해도 의도하지 않은 외부 패키지를 가져오는 상황을 막는 역할을 합니다.
브랜치 병합도 흔한 원인입니다. 두 사람이 각각 다른 패키지를 추가하면 package.json과 pnpm-lock.yaml이 동시에 충돌할 수 있습니다. 이때 lockfile 충돌 표시만 제거하면 YAML 문법은 맞아도 실제 의존성 그래프가 깨진 상태로 남을 수 있습니다. 충돌 해결 후에는 다시 설치 명령을 실행해 lockfile이 실제로 유효한지 확인해야 합니다.
로컬, CI, Docker에서 점검하는 순서
설치 오류를 볼 때는 “삭제 후 재설치”보다 “어떤 기준이 어긋났는지”를 먼저 찾는 쪽이 안정적입니다. 아래 순서대로 보면 대부분의 lockfile, workspace 오류는 원인이 좁혀집니다.
첫 번째는 변경 파일 확인입니다. 최근 커밋에서 package.json만 바뀌고 pnpm-lock.yaml이 빠졌는지 봅니다. 패키지 추가, 삭제, 버전 변경이 있었다면 lockfile 변경도 같이 따라오는 것이 일반적인 상태입니다.
git diff --name-only HEAD~1..HEAD
git diff -- package.json pnpm-lock.yaml
두 번째는 workspace 범위 확인입니다. 앱은 apps/admin에 있는데 pnpm-workspace.yaml에는 packages/*만 들어 있으면 앱 쪽 package.json은 workspace 구성 안에서 기대한 방식으로 처리되지 않습니다. 반대로 더 이상 쓰지 않는 폴더가 workspace에 남아 있으면 불필요한 설치 대상이 늘어납니다.
pnpm list -r --depth 0
세 번째는 내부 패키지 이름 확인입니다. 모노레포에서는 폴더명과 패키지명을 다르게 쓰는 경우가 흔합니다. packages/ui 폴더 안의 package.json 이름이 @site/design-system이라면, 사용하는 쪽에서도 그 이름으로 의존성을 적어야 합니다.
네 번째는 CI의 설치 명령 확인입니다. pnpm은 CI 환경에서 lockfile이 업데이트되어야 하는 상태를 더 엄격하게 다룹니다. 그래서 로컬에서 통과한 명령과 CI에서 실행하는 명령을 맞춰두는 편이 좋습니다. 배포 전에 로컬에서도 pnpm install --frozen-lockfile을 한 번 실행하면 누락된 lockfile 변경을 미리 찾을 수 있습니다.
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build
다섯 번째는 Docker 빌드 순서입니다. Dockerfile에서 package.json만 복사하고 lockfile이나 workspace 설정 파일을 누락하면 로컬과 다른 설치 결과가 나옵니다. 모노레포라면 각 패키지의 package.json이 설치 전에 복사되는지도 같이 봐야 합니다.
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY apps/admin/package.json ./apps/admin/package.json
COPY packages/ui/package.json ./packages/ui/package.json
RUN pnpm install --frozen-lockfile
여섯 번째는 로컬 설치 산출물 정리입니다. 앞의 기준이 모두 맞는데도 설치 결과가 이상하다면 그때 node_modules와 store를 정리합니다. 이 단계에서도 pnpm-lock.yaml은 먼저 남겨두고 확인하는 편이 원인 추적에 유리합니다.
rm -rf node_modules
pnpm store prune
pnpm install
Windows PowerShell에서는 삭제 명령이 다릅니다. 프로젝트 루트에서 아래처럼 정리할 수 있습니다.
Remove-Item -Recurse -Force node_modules
pnpm store prune
pnpm install
기본 점검 후에도 실패할 때 볼 케이스
기본 점검을 해도 계속 실패한다면 조금 더 좁은 조건을 봐야 합니다. 첫 번째는 private package나 사내 registry입니다. 인증 토큰이 로컬에는 있는데 CI에는 없으면 workspace 문제가 아닌 registry 인증 문제처럼 보일 수 있습니다. 이 경우 에러 메시지에 401, 403, registry URL, scope 정보가 함께 나오는지 확인합니다.
pnpm config get registry
pnpm config get @my-scope:registry
토큰이 필요한 registry를 쓰는 경우, 인증 정보는 코드 저장소에 직접 커밋하지 않아야 합니다. 로컬에서는 .npmrc 때문에 통과하고 CI에서는 실패하는 형태가 자주 나오므로, CI secret에 토큰이 들어 있는지와 설치 단계에서 해당 secret을 실제로 읽는지 확인합니다.
두 번째는 file:, link: 의존성입니다. 특정 로컬 경로를 참조하는 의존성이 있는데 CI나 Docker 빌드 컨텍스트에 그 파일이 없다면 설치가 실패합니다. 로컬 PC에는 파일이 있으니 문제가 없어 보이지만, 배포 환경에는 해당 경로가 없을 수 있습니다.
{
"dependencies": {
"@site/local-tool": "file:../local-tool"
}
}
위와 같은 의존성은 개인 로컬에서만 존재하는 경로를 참조하지 않는지 봐야 합니다. 모노레포 안에서 관리할 패키지라면 workspace 패키지로 포함시키고, 외부에 있는 도구라면 registry 배포나 별도 설치 절차를 정리하는 쪽이 낫습니다.
세 번째는 lockfile 충돌 해결 후 검증 누락입니다. 충돌 표시만 제거했다고 lockfile이 정상이라는 뜻은 아닙니다. 충돌을 해결한 뒤에는 설치 명령을 다시 실행하고, lockfile 변경이 과하게 커지지 않았는지 확인해야 합니다.
pnpm install --frozen-lockfile
git diff --stat pnpm-lock.yaml
다음 작업에서 남겨둘 체크리스트
pnpm install 오류를 줄이려면 설치가 실패했을 때의 대응보다, 의존성을 바꾸는 순간의 규칙을 정해두는 것이 더 효과적입니다. 패키지를 추가하면 package.json과 pnpm-lock.yaml이 함께 바뀌는지 확인하고, workspace 패키지를 추가하면 pnpm-workspace.yaml 범위와 package.json name까지 같이 점검합니다.
- 의존성 변경 PR에서는
package.json과pnpm-lock.yaml변경을 함께 확인합니다. - 루트 package.json의
packageManager로 pnpm 버전을 고정합니다. - workspace 내부 패키지는
workspace:프로토콜로 로컬 패키지 사용 의도를 드러냅니다. - CI에서는
pnpm install --frozen-lockfile조건으로 lockfile 누락을 빨리 드러냅니다. - lockfile 충돌을 해결한 뒤에는 설치 명령을 다시 실행해 실제로 재현되는지 확인합니다.
- Docker 빌드에서는 lockfile, workspace 설정 파일, 각 패키지의 package.json 복사 순서를 확인합니다.
다음에 같은 오류를 만나면 lockfile 삭제부터 시작하지 않는 것이 가장 큰 기준입니다. 삭제는 해결책이 될 수 있지만 진단 방법은 아닙니다. 어떤 파일이 어떤 이유로 바뀌어야 하는지 확인한 다음 lockfile을 갱신해야 팀 작업과 배포 환경에서도 같은 결과를 유지할 수 있습니다.
검증할 때는 pnpm의 install 옵션, workspace protocol, pnpm-workspace.yaml 설정, Docker 설치 예시를 같이 보면 됩니다. 버전별로 CI 동작이나 lockfile 호환성 검증이 달라질 수 있으므로, 실제 프로젝트에서는 packageManager에 고정한 pnpm 버전과 CI에서 실행되는 pnpm 버전을 맞추는 것까지 확인해야 합니다.