AI 사용기
AI 활용기: 내 테스트가 남의 프로젝트 폴더에 빚지고 있었다
배포 전 QA 게이트가 자기 package.json엔 없는 패키지를, '정리 예정' 표시가 붙은 이웃 프로젝트의 node_modules에서 상대경로로 빌려 쓰고 있었다. 같은 날 다른 저장소에서 같은 함정을 다시 만나 바로 고쳤다.
여러 저장소를 오가며 AI 에이전트에게 테스트·빌드 설정을 맡기는 사람에게, 왜 '어쩌다 보니 동작하는' 의존성이 나중에 저장소 경계를 넘어 조용히 무너지는지 구체적 사례로 보여준다.
독립된 두 게임 스타터 파이프라인 저장소를 같은 날 점검하다가, 한쪽의 배포 전 QA 게이트(라이브 스모크 테스트)가 자기 package.json에 dependencies로 선언하지 않은 채 다른 프로젝트의 uploaded 폴더 안 node_modules/playwright를 상대경로로 직접 import해서 돌아가고 있는 걸 발견했다. 그 폴더는 '언젠가 정리될 폴더'로 분류돼 있어서, 누군가 정리하는 순간 이 저장소의 배포 게이트 4종이 통째로 죽는 구조였다. 같은 교훈을 다른 저장소에서 곧바로 적용해, 이번엔 처음부터 자체 devDependency로 playwright를 설치했다.
여러 저장소를 함께 다룰 때는 테스트나 스크립트가 import하는 경로가 자기 package.json의 dependencies에 선언돼 있는지 주기적으로 점검한다. 선언 없이 동작하는 import를 발견하면 그 자리에서 자체 devDependency로 옮긴다.
먼저 답하면
게임 스타터를 자동으로 찍어내는 파이프라인 저장소의 배포 전 QA 게이트가, 자기 package.json엔 없는 패키지를 “정리 예정”이라고 표시된 이웃 프로젝트의 node_modules에서 상대경로로 빌려 쓰고 있었다. 그 폴더가 정리되는 순간 배포 게이트 전체가 죽는 구조였다.
재현 방법
게임 스타터를 대량 생산하는 두 개의 독립된 저장소를 같은 날 각각 점검했다. 한쪽에는 배포 직전에 실행되는 QA 게이트가 있는데, 라이브 스모크 테스트 4종이 브라우저를 띄워 실제 화면을 확인하는 방식이었다. 코드 리뷰 중 “이 테스트가 왜 이 경로를 import하지?“를 따라가 보니, import문이 자기 저장소가 아니라 완전히 다른 프로젝트(app-factory)의 uploaded/merge-whale/... 아래 있는 node_modules/playwright를 상대경로로 직접 가리키고 있었다.
문제는 그 저장소의 package.json에는 dependencies 필드 자체가 없었다는 것이다. playwright는 어디에도 선언돼 있지 않았다. 그런데도 테스트는 잘 돌아갔다 — 옆 프로젝트가 이미 설치해둔 playwright를 상대경로로 그냥 빌려 쓰고 있었기 때문이다. 게다가 그 옆 프로젝트의 폴더는 “동결 예정, 언젠가 정리될 폴더”로 따로 분류돼 있었다. 즉 누군가 정리 작업을 하는 순간, 이 저장소의 배포 게이트 4종이 아무 코드 변경 없이 통째로 “모듈 없음” 에러로 죽는 구조였다.
검증 결과
같은 날 다른 세션에서 두 번째 독립 저장소(kein-game-forge)를 다루다가, 방금 배운 교훈을 그대로 적용했다. 이번에는 애초에 playwright를 그 저장소 자신의 devDependency로 설치했다. 실제 커밋 메시지에도 “add playwright as this repo’s own devDependency (not borrowed from a sibling project’s node_modules)“라고 이유를 명시했다.
두 경우 모두 실제 장애가 터진 건 아니었다 — 현재 배포 상태는 44/44 그린이다. 하지만 “선언 안 된 패키지가 이웃 폴더의 설치 결과에 얹혀서 어쩌다 보니 동작하는” 상태는, 그 이웃 폴더가 조금이라도 바뀌면 내 코드는 한 줄도 안 바꿨는데 빌드가 깨지는 잠재 리스크였다.
한계와 다음 개선
이 유형은 JS 생태계에서 이미 이름이 붙어 있다. Rush.js 공식 문서는 이를 “phantom dependency”라 부른다 — 자기 package.json에 선언하지 않은 패키지를, 다른 패키지가 설치 과정에서 상위 폴더로 끌어올린(hoist) 덕분에 우연히 import할 수 있게 되는 상태를 가리킨다. 그 패키지 구성이 바뀌면 hoist 레이아웃이 흔들려서 내 코드는 그대로인데 빌드가 깨진다는 설명이 이번 사례와 정확히 겹친다.
npm/cli 저장소의 이슈 “Advice on dependency hoisting in monorepos”(https://github.com/npm/cli/issues/7167)에서도 같은 메커니즘이 실무 사고 사례로 다뤄진다. pnpm 계열 도구들이 기본값으로 “isolated” 링크 방식을 쓰는 이유도 애플리케이션 코드가 선언 안 된 의존성에 아예 접근하지 못하게 구조적으로 막기 위해서다.
Remotion 저장소의 이슈 #11269도 대칭적인 사례를 보여준다. 한 패키지의 변경이 다른 패키지 테스트가 소비하던 파일을 지웠는데, 영향받는 테스트만 골라 돌리는 선택 로직이 이 저장소 경계를 넘는 의존을 놓쳐서 CI가 못 잡아냈다는 내용이다. 저장소 경계를 넘는 암묵적 의존이 자동화 게이트의 사각지대가 된다는 점에서, 이번에 발견한 문제와 같은 구조다.
세 자료를 같이 보면 정리는 하나로 모인다 — 여러 저장소를 오가며 AI 에이전트에게 테스트·스크립트 설정을 맡길 때는, “지금 동작한다”와 “이 저장소만으로 동작한다”를 구분해서 확인해야 한다. import 경로가 자기 package.json에 선언된 패키지를 가리키는지 주기적으로 점검하지 않으면, 편의상 만든 지름길이 저장소 경계를 몰래 넘어 감사할 때만 드러나는 빚으로 쌓인다.