Phantom Dependency에 대해서 알아보고 대비책 생각해보기 👻

Phantom Dependency란?

팬텀 의존성(Phantom Dependency)은 프로젝트의 package.json 파일에 명시되지 않았지만 빌드 타임이나 런타임에 의존성을 가지게 되는 상황을 의미한다.

이 문제는 주로 Node.js의 모듈 평탄화(flattening)로 인해 발생하게 되는데 NPM이나 Yarn이 의존성을 설치할 때, 하위 의존성을 같은 node_modules 폴더에 배치하기 때문에 주로 발생한다.

(모듈 평탄화는 중복해서 설치되는 패키지를 줄이기 위해서 설치한 패키지들이 모두 맨 위로 hoisting 되는 것을 의미한다)

 

😅 쉽게 말해, 우리 코드가 직접적으로 사용하지 않는 패키지에 ‘슬며시’ 의존하고 있는 상태가 팬텀 의존성이다.

 

예제를 통해서 확인해보면 다음과 같다.

{ 
    "dependencies": { 
    	"minimatch": "^3.0.4" 
    } 
}

위와 같은 package.json이 있고

const minimatch = require('minimatch'); // OK, package.json에 있음
const braceExpansion = require('brace-expansion'); // ???
var glob = require('glob'); // ???

위와 같이 작성된 코드가 있다.

 

그런데 brace-expansion과 glob은 minimatch의 하위 의존성으로 설치되었을 뿐, package.json에는 명시되지 않았다.

그런데도 코드에서 사용이 가능한 이유가 바로, phantom dependency 때문이다 👻

 

왜 문제일까 😭

팬텀 의존성은 다음과 같은 문제를 초래할 수 있다 ❗

 

디버깅의 어려움

  • 에러의 원인이 프로젝트 코드나 명시된 의존성에서 오는 것이 아니라, 보이지 않는 의존성에 있는 경우 문제를 찾기가 어렵다

의존성 관리 혼란

  • 프로젝트를 다른 팀원이 복제해 사용하려 할 때, 의존성 문제가 발생할 수 있다
  • 명시적으로 package.json에 없는 의존성은 깨지기 쉽다
  • 빌드 환경에서 누락된 패키지가 생겨 빌드가 실패할 수도 있다
  • 로컬 개발 환경과 배포 환경 간에 간접 의존성의 버전 차이로 인한 에러가 발생할 수도 있다

업데이트 리스크

  • 하위 의존성의 업데이트가 예기치 못한 오류를 유발할 수 있다
  • 하위 의존성은 직접 제어할 수 없으므로 버전 충돌이나 동작 변경의 가능성이 높아진다

 

Phantom Dependency는 그럼 어떻게 예방할 수 있을까 ❓

팬텀 의존성을 예방하거나 해결하기 위해 아래 방법을 활용할 수 있다.

 

명시적 의존성 추가 ✨

  • 이 방법은 코드에서 사용하는 모든 의존성을 package.json에 명시적으로 추가하는 것이다.
    • 협업 및 배포 과정에서 발생하는 Phantom Dependency 문제를 예방할 수 있다.
  • npm install package-name 명령어를 사용해 package.json에 직접 추가하면 된다.
  • 위 예시에서는 brace-expansion나 glob이 예기치못하게 사용이 가능한데, 해당 라이브러리들도 dependency에 적어주는 것이다.
npm install brace-expansion glob

위와 같은 명령어로 설치하면,

{
  "dependencies": {
    "brace-expansion": "^1.1.8",
    "glob": "^11.0.0"
  }
}

사용가능한 모든 라이브러리를 dependency에 포함한다.

(물론, 해결책으로 이 방법을 원하는 것은 아닐테지만.... 💧 그래도 모르고 설치된 라이브러리도 명시해주는 것은 의미 있다)

 

+ npm ls 명령어를 사용하여 설치된 모든 의존성의 트리를 확인할 수 있다.

npm ls rollup

 

결과는 다음과 같이 나온다.

 

+ webpack-bundle-analyzer와 같은 라이브러리를 사용하면 시각적으로 사용된 패키지를 확인할 수 있고, 예기치않게 추가된 패키지도 확인할 수 있다.

 

+ Rush.js 사용

  • Rush.js는 팬텀 의존성을 감지하고 경고 메시지를 출력해준다
  • 프로젝트 빌드 단계에서 팬텀 의존성을 자동으로 체크해 문제를 예방할 수도 있다
rush check

 

CI/CD 파이프라인에 의존성 검증 추가

  • npm ci 명령어와 같은 클린 설치(clean install) 방식을 통해 의존성을 검증하자
    • npm ci 명령어를 사용하면 기존의 node_modules를 무시하고, package.json과 package-lock.json혹은 yarn.lock에 명시된 의존성만을 설치하여 팬텀 의존성 문제가 발생할 여지를 줄여준다.
    •  👍프로젝트에서 동일한 의존성이 설치될 수 있도록 package-lock.json나 yarn.lock을 커밋할 수 있도록 하자.
  • lint 툴과 스크립트를 사용해 명시되지 않은 의존성을 감지할 수도 있다
    • eslint-plugin-import와 같은 플러그인을 통해 설치되지 않은 의존성을 검사할 수 있다
    • npm-check와 같은 도구를 통해서 의존성 상태를 점검할 수도 있다

 

팬텀 의존성은 "작동은 하지만 위험한 요소"라고 할 수 있다.

단기적으로는 문제가 없을 수 있지만, 프로젝트가 커지고 협업이 늘어날수록 예상치 못한 문제를 초래할 가능성이 크다.

+ 특히 여러 프로젝트가 동일한 의존성을 공유하는 모노레포 구조에서 취약할 수 있다 😭

 

패키지를 추가하거나 삭제하는 등 의존성에 변경사항이 발생할 땐 항상 명시적으로 패키지를 추가하고 늘 의존성에 대해서 경각심을 갖자 👻

 

+ pnpm이나 yarn berry는 구조상 팬텀 디펜던시 문제를 예방할 수 있다. (.pnpm 및 Plug'n'Play (PnP) 방식으로 인해)

 

👍 참고 자료

https://rushjs.io/pages/advanced/phantom_deps/

 

Phantom dependencies | Rush

Rush's documentation occasionally mentions "phantoms" and "doppelgangers".

rushjs.io

https://www.npmjs.com/package/npm-check

 

npm-check

Check for outdated, incorrect, and unused dependencies.. Latest version: 6.0.1, last published: 2 years ago. Start using npm-check in your project by running `npm i npm-check`. There are 255 other projects in the npm registry using npm-check.

www.npmjs.com

https://github.com/import-js/eslint-plugin-import

 

GitHub - import-js/eslint-plugin-import: ESLint plugin with rules that help validate proper imports.

ESLint plugin with rules that help validate proper imports. - import-js/eslint-plugin-import

github.com

https://toss.tech/article/node-modules-and-yarn-berry

 

node_modules로부터 우리를 구원해 줄 Yarn Berry

토스 프론트엔드 레포지토리 대부분에서 사용하고 있는 패키지 매니저 Yarn Berry. 채택하게 된 배경과 사용하면서 좋았던 점을 공유합니다.

toss.tech