Medium
웹 의존성은 망가졌습니다. 고칠 수 있을까요?
left-pad 사태 10주년. npm 트리가 수백~수천 노드로 번식하는 구조적 원인과 lockfile/벤더링/URL 임포트 같은 대안을 방어 레이어별 로 정리한다.
핵심 요약
npm 생태계는 작은 패키지를 많이 조합하는 문화가 지배적이어서 직접 의존성 N개가 전이 의존성 수백~수천 으로 확장된다. 이 구조는 네 가지 리스크를 낳는다. (1) 공급망 공격 — 유지보수자 계정 탈취로 악성 버전 배포, (2) 의존성 지옥 — peer 불일치로 빌드 불가, (3) 재현 불가 — lockfile 이 없거나 무시되어 같은 코드가 다른 바이너리로 빌드됨, (4) 성능 부채 — 번들 크기/콜드 스타트 증가. 해법은 단일 실버불릿이 아니라 감시(npm audit, Snyk) + 고정(lockfile, integrity hash, SBOM) + 축소(vendoring, 내장 유틸로 대체, Deno URL 임포트) 의 다층 방어다.
의존성 문제를 '나쁜 패키지 vs 좋은 패키지' 가 아니라 '트리의 깊이와 면적' 으로 본다. package.json 의 직접 의존성 10개가 전이 의존성 수백 개 로 확장되는 지점이 실제 공격 면적이다. 해법은 항상 감시 / 고정 / 축소 세 층의 조합이다.
신입-중급 면접에서 'npm audit 돌려봤어요'로 끝내면 얕다. 실제로 물어볼 만한 맥락은 이것이다.
left-pad(2016),event-stream(2018),ua-parser-js(2021) — 모두 작은 전이 의존성 한 줄이 원인lockfile이 있어도npm install이 여전히 해시를 재검증하지 않으면 의미가 없음- Deno/Bun/pnpm 이 가진 URL 임포트 / 엄격한 peer 검증 이 어떤 문제를 해결하는지 설명할 수 있어야 함
학습 포인트
면접 답변으로 연결할 학습 포인트입니다.
전이 의존성 폭발은 구조 문제
package.json 의 10줄이 실제로는 수천 줄짜리 그래프 가 된다. npm ls --all | wc -l 로 확인해보면 빈 Express 프로젝트도 수백 패키지를 끌어온다.
# 실제 그래프 크기 확인
npm ls --all --json | jq '[.. | .version? // empty] | length'
해결은 직접 의존성 수 가 아니라 전이 의존성 수 를 KPI로 잡는 것. 같은 기능이라면 date-fns 대신 Intl.DateTimeFormat, lodash.get 대신 ?. optional chaining 으로 대체 가능한 것부터 제거.
package.json 상단만 보고 '깔끔하다' 고 판단. 노드 모듈 폴더 크기 와 전이 개수 가 진짜 지표다.
lockfile 은 '있다' 가 아니라 '검증된다'
package-lock.json / yarn.lock / pnpm-lock.yaml 은 버전만이 아니라 integrity hash(SRI) 를 담는다. CI 에서 꼭 --frozen-lockfile(yarn/pnpm) 또는 npm ci 를 써야 해시 검증이 강제된다.
| 명령 | lockfile | integrity 검증 |
|---|---|---|
npm install | 읽고 갱신 | 부분 검증 |
npm ci | 읽기 전용 | 전수 검증 |
yarn install --frozen-lockfile | 읽기 전용 | 전수 검증 |
pnpm install --frozen-lockfile | 읽기 전용 | 전수 검증 |
운영 빌드는 반드시 ci / frozen 계열로.
lockfile 만 커밋해두면 끝이라 생각하는 것. CI 에서 install 쓰면 공격자가 바뀐 tarball 을 올렸을 때 그대로 들어온다.
대안: 축소 vs URL 임포트
공급망 위험을 구조적으로 낮추는 두 방향이 있다.
- 축소(vendoring): 작은 유틸리티는
node_modules/가 아니라 소스 트리 에 복사해 들인다. 감시 주체가 본인이 됨 - URL 임포트(Deno/Bun):
import x from 'https://esm.sh/pkg@1.2.3'처럼 버전 + 해시 가 URL 에 박혀, 레지스트리 계정 탈취와 무관
// Deno: 해시까지 고정하면 MITM 도 방어
import { z } from 'https://esm.sh/zod@3.22.4?dts';
둘 다 만능은 아니다 — vendoring 은 업데이트 부담, URL 임포트는 CDN 가용성 의존.
'Deno 쓰면 끝난다' 는 식의 단일 해법 사고. 실무는 npm 유지 + lockfile + SCA 스캐너 + 꾸준한 전이 축소 조합이 현실적.
읽는 순서
- 1이론
npm공식 문서의npm-ci/package-lock.json섹션과 OpenSSF 의 '10 Best Practices for Open Source Supply Chain' 을 읽고 용어를 정리하세요. - 2구현
본인 프로젝트에서
npm ls --all --json | jq '[.. | .version? // empty] | length'로 전이 의존성 수를 측정하고, Top 5 큰 패키지를 bundlephobia 에 넣어 번들 영향을 기록하세요. - 3실무
CI 설정에서
install→ci로 바꾸고,npm audit --production을fail on high로 강제하는 게이트를 PR 로 추가하세요. - 4설명
'우리 팀이
left-pad사태를 맞으면 몇 분 안에 탐지/롤백할 수 있는가' 를 주제로 런북 을 1장으로 작성해 팀에 공유하세요.
면접 연결 질문
[감점 답변] 'ci 가 더 빠르다'. [좋은 답변] 세 축: (1) lockfile 쓰기 여부 (install=갱신/ci=읽기), (2) integrity hash 전수 검증 여부, (3) node_modules 초기화 여부. CI 에서는 반드시 npm ci — 공격자가 tarball 을 바꿔도 해시 불일치로 차단.
[감점 답변] '안 쓰면 된다'. [좋은 답변] 단계적: (1) lodash 호출 지점 grep 으로 전수 조사, (2) ES2020 대체 가능 여부 테이블화(_.get → ?., _.map → Array#map), (3) babel-plugin-lodash 로 부분 임포트 로 임시 축소, (4) 호출이 <10곳이면 전수 제거 PR, (5) bundlephobia 로 전후 번들 크기 비교.
[감점 답변] '레지스트리가 없어서'. [좋은 답변] 근거: URL 에 버전이 고정 되고, deno.lock 에 integrity hash 가 박혀 MITM/공격 업데이트가 차단. 한계: (1) CDN 가용성에 가용성이 의존, (2) 타입 정의 해석 비용, (3) 오프라인 빌드에 별도 vendor 필요.
자기 점검
직접 의존성만 세는 것. 실제 공격 면은 전이 의존성 수 다.
lockfile 만 있으면 충분하다는 오해.
완전 1:1 대체라 여기는 것 — _.get(obj, 'a.b', fallback) 는 falsy 값에도 fallback 을 반환한다.