착수 시점: 열 개 레포, 228건
GitHub Dependabot 의 열린 경보를 전 레포 조회하니 이랬습니다.
| 레포 | 경보 | 패키지 매니저 |
|---|---|---|
| 블로그 | 91 (high 50) | pnpm |
| 크롤러 A | 60 (high 28) | pnpm |
| 포트폴리오 | 16 (critical 1 · high 8) | yarn 4.5.0 |
| 그 외 7개 | 61 | npm / pnpm / uv |
| 합계 | 228 |
여기서 첫 번째로 배운 것. 경보가 꺼져 있는 레포는 이 표에 아예 안 나옵니다. 조회가 안 되니까요. "목록에 없다 = 안전하다" 가 아니라 "목록에 없다 = 안 쟀다" 입니다. 실제로 경보 기능이 꺼져 있던 레포가 일곱 개 더 있었습니다.
npm audit fix 로 안 끝나는 이유
대부분의 취약 패키지가 상위 패키지에 정확한 버전으로 핀돼 있었습니다.
prisma -> mysql2: 3.15.3 (필요: >=3.22.0)
@prisma/config -> deepmerge-ts: 7.1.5 (필요: >=8.0.0)
@serwist/next -> browserslist: 4.28.6 (필요: >=4.28.7)
캐럿(^)이 아니라 고정값이라 audit fix 는 손을 못 댑니다.
(괄호 안 하한은 2026년 9월 8일 당시 경보 기준입니다. 권고는 계속 갱신되니
지금 이 숫자를 안전선으로 쓰면 안 됩니다 — 예컨대 mysql2 는 그 뒤
<=3.23.0 을 대상으로 하는 권고가 하나 더 들어왔습니다.) 더 인상적이었던 건
npm 이 제안한 "해결책"이었어요. prisma 를 다운그레이드하라는 겁니다.
상위를 내리면 그 취약한 트리 자체가 사라지니까 계산상으로는 맞죠.
받으면 안 되는 제안입니다.
남은 길은 overrides / resolutions 로 강제하는 것뿐이었습니다.
그리고 거기서부터 재미있어집니다.
함정 ① 상한 없는 >= 는 메이저를 넘어갑니다
권고가 "undici 7.29.0 이상"이라기에 그대로 적었습니다.
"undici": ">=7.29.0"
깔린 건 undici 8.10.2 였습니다. jsdom 은 ^7.24.5 를 요구하면서
undici/lib/handler/wrap-handler.js 를 직접 require 하는데, v8 에 그 경로가 없습니다.
Test Files no tests
Errors 9 errors
테스트 9파일이 통째로 MODULE_NOT_FOUND 로 죽었습니다. 취약점을 고치려다
테스트 스위트를 날린 거죠.
이후 제가 추가한 override 전부에 <다음메이저 상한을 걸었습니다. 트리 안에
메이저가 둘 이상 섞인 패키지(js-yaml 3·4·5, ws 7·8, brace-expansion 1·2·5)는
한 줄로 뭉개면 v6 소비자가 v7 을 받습니다. 셀렉터로 갈라야 합니다.
같은 사고가 그 뒤로 두 번 더 났습니다. >= 하나가 ws@^7 소비자까지 v8 로 끌어올렸고,
또 하나는 정확히 핀된 1.19.11 을 2.1.1 로 갈아치웠습니다.
0.x 는 더 위험합니다 — 0.41 → 0.42 는 semver 상 마이너지만 실질은 메이저예요.
함정 ② 로컬 audit 0건은, CI 가 같은 락파일을 쓸 때만 의미가 있습니다
다 고치고 푸시한 뒤 경보를 다시 쟀더니 블로그 레포만 42건이 남아 있었습니다.
로컬 pnpm audit 은 0인데요.
경보의 manifest_path 를 봤습니다.
package-lock.json = 42
.github/workflows/ci.yml 의 잡 다섯 개가 전부 npm ci 였습니다. 그리고 그
package-lock.json 은 7월 4일 이후로 갱신이 없었습니다.
즉 제 pnpm 수정은 CI 에 한 줄도 안 닿았고, 42건은 허수가 아니라 CI 가 실제로 설치하는 트리였습니다. 로컬에서 초록인 것과 CI 에서 초록인 것은 다른 사건입니다.
락파일을 재생성하니 두 가지가 더 나왔습니다.
- npm 은 override 가 직접 의존성과 어긋나면
EOVERRIDE로 막습니다. pnpm 은 안 막아요. 덕분에 pnpm 쪽에 조용히 쌓여 있던 모순 두 건이 드러났습니다. - 낡은 락파일이 잠재 버그를 가리고 있었습니다.
SyntaxError: The requested module 'brace-expansion'
does not provide an export named 'expand'
at glob/node_modules/minimatch/dist/esm/index.js:1
expand 명명 export 는 brace-expansion v5 에만 있고 minimatch 10 이 그걸 씁니다.
낡은 락이 minimatch 9 를 물고 있어서 안 보였을 뿐이에요. 락파일을 오래 안 올리면
문제가 사라지는 게 아니라 안 보이게 됩니다.
함정 ③ 3주간 초록이던 파이프라인 안에서, 한 잡이 한 번도 안 돌았습니다
제 커밋에서 한 레포의 server-ai 테스트가 커버리지 임계(branch 70%)를 69.21% 로
못 넘겨 실패했습니다. 직전 런은 분명 success 였는데요. 런 목록을 잡 단위로 폈습니다.
2026-09-08 run=failure server-ai=failure ← 내 커밋
2026-09-06 run=success server-ai=skipped
2026-09-01 run=failure server-ai=skipped
... 2026-08-17 까지 15런 전부 server-ai=skipped
경로 필터가 걸러서 3주 넘게 한 번도 안 돌았습니다. 제가 그 디렉터리를 건드리는 바람에 처음 실행된 거예요.
여기서 정확히 해두면, 파이프라인 전체가 놀고 있었던 건 아닙니다. 9월 1일 런은 failure 고
다른 잡들은 정상으로 돌았어요. 문제는 이 잡만 계속 skipped 였다는 것이고,
런 단위 conclusion 만 봐서는 그게 안 보인다는 겁니다. skipped 는 빨간불이 아니니까요.
제 변경 탓인지도 갈라봤습니다. 부모 커밋의 spec 파일로 되돌려 돌려도 69.21% 로 동일합니다. 회귀가 아니라 원래 미달이 드러난 것이었어요.
그래서 안 고쳤습니다. 커버리지 미달은 의존성 문제가 아니고, 숫자를 넘기려고 테스트를 덧대는 건 제일 하면 안 되는 일입니다. 미달 지점만 기록해 뒀습니다. 0.79%p 차이예요.
곁다리: AI 리뷰가 인용한 GHSA 네 개가 그때는 없었습니다
이 작업은 다른 모델에게 교차 리뷰를 받았습니다. VERDICT: REVISE 와 함께
지적이 여럿 왔고, 그중 GHSA 번호를 인용한 게 여섯 건이었습니다.
조회한 2026-09-08 04:41 UTC 기준으로 넷이 404 였습니다.
GHSA-rfgv-xxqx-mfg5 (undici) → 404
GHSA-rj75-hqrm-r3gf (postcss-selector-parser) → 404
GHSA-qw65-cvwx-89v3 (fast-uri) → 404
GHSA-g6gw-c38x-mqfc (hono) → 404
실재하는 나머지 둘은 진짜였지만 이미 충족돼 있었습니다.
이 글을 쓰면서 넷을 다시 조회했는데, 여기서 재미있는 게 나왔습니다.
hono 건은 지금 실재합니다. 공개 시각이 2026-09-08T21:23:02Z — 제가 조회한
그날, 16시간 뒤였어요. 나머지 셋은 일주일이 지난 지금도 404 입니다.
그러니 정확한 서술은 "네 개가 가짜였다"가 아니라 "조회 시점에 확인할 수 없는 것을 근거로 댔다" 입니다. 하나는 나중에 맞는 것으로 판명됐고, 셋은 아직도 아닙니다. 어느 쪽이든 판정 시점에 확인 안 되는 근거로 코드를 고칠 수는 없습니다.
그렇다고 그 리뷰가 쓸모없었냐면 아닙니다. 같은 리뷰가 메이저 뭉갬 3건을 정확히 짚었고, 그건 재현돼서 전부 고쳤습니다. 제가 만든 결함이었어요.
교훈은 "AI 리뷰를 믿지 마라"가 아니라 이겁니다. 지적은 포인터지 증거가 아닙니다.
전부 대조해야 하고, 대조할 때는 계측기부터 교정해야 합니다. 저는 GitHub 권고 API 를
기준으로 삼기 전에 양성 표본(undici@7.25.0 → 권고 5건)과 음성 표본(undici@7.29.1 → 0건)으로
그 API 가 제대로 답하는지 먼저 확인했습니다. 그다음에야 락파일에서 뽑은 66개 버전을
전수 조회했고요.
결과
228건 → 3건. 남은 셋은 전부 업스트림에 패치된 릴리스가 없는 것들입니다. override 로 우회할 수 있는 종류가 아니라, 상위 패키지가 릴리스를 내야 닫힙니다.
부수적으로 이런 것들이 같이 드러났습니다.
- 포맷 검사 한 줄 실패로 8월 20일 이후 배포가 계속 skip 되던 레포 (지금 이 블로그입니다)
- 8월 2일부터 수집 단계에서 죽고 있던 Python 테스트 — 역시 상한 없는
>=였습니다 disabled_manually상태로 꺼져 있던 워크플로 — 왜 껐는지 기억이 안 나서 안 켰습니다
남는 것
세 줄로 줄이면 이렇습니다.
>=만 쓴 override 는 시한폭탄입니다. 이번에 셋이 터졌으니 나머지도 언젠가 터집니다.- 로컬 audit 0건은 CI 가 같은 락파일을 쓸 때만 유효합니다. 아니면 그냥 다른 프로젝트를 잰 겁니다.
- 초록불을 보고하기 전에 무엇이 실제로 돌았는지 세야 합니다. 오케스트레이터는 스크립트가 없는 워크스페이스를 조용히 건너뛰고, 건너뛴 것과 통과한 것이 같은 exit 0 으로 보입니다.
세 달 만에 쓴 글이 의존성 이야기인 게 좀 웃기지만, 사실 이 글을 쓸 수 있게 된 것도 그 정비 덕분입니다. 포맷 한 줄 때문에 배포가 막혀 있었으니까요.