npm left-pad 사태 (2016) — 11줄 코드가 세상을 멈추다

npm left-pad 사태 (2016) — 11줄 코드가 세상을 멈추다

한 개발자의 분노, 패키지 삭제 하나로 React, Babel, 수천 개 프로젝트가 동시에 빌드 실패한 사건


2016년 3월 22일. 전 세계 JavaScript 개발자들이 동시에 이런 메시지를 보게 됨:

npm ERR! 404 'left-pad' is not in the npm registry.
npm ERR! 404 You should bug the author to publish it
npm ERR! 404 (or use the name yourself!)

React가 빌드 실패. Babel이 빌드 실패. 수천 개의 Node.js 프로젝트가 동시에 빌드 실패. 원인은? 11줄짜리 패키지 하나가 npm에서 삭제된 것.

사건 요약

터키 출신 개발자 Azer Koçulu가 npm에서 자신의 모든 패키지(약 250개)를 삭제함. 이 중 하나인 left-pad(문자열 왼쪽 패딩 함수, 11줄)에 직접 또는 간접적으로 의존하던 수천 개의 패키지가 동시에 설치 실패. npm 생태계 전체가 약 2.5시간 동안 혼돈 상태에 빠짐.


발단: Azer Koçulu vs. npm, Inc.

kik 메신저와의 이름 분쟁

사건의 시작은 left-pad가 아니라 kik이라는 패키지명 분쟁이었음.

Azer Koçulu는 npm에 kik이라는 이름의 패키지를 퍼블리시해서 사용하고 있었다. 그런데 어느 날, 메신저 앱 Kik의 변호사로부터 이메일이 옴:

From: kik-legal@kik.com
Subject: kik npm package

안녕하세요, 저희는 Kik Interactive의 법무팀입니다.
'kik'이라는 npm 패키지명은 저희 상표권을 침해합니다.
패키지명을 변경해주시기 바랍니다.

Azer의 대답은 단호했음: "No."

Azer의 입장

Azer는 오픈소스 개발자로서, 기업이 개인의 패키지명을 빼앗으려는 것에 강하게 반발했음. "대기업이 돈과 변호사를 앞세워 개인 개발자의 것을 빼앗는 것은 부당하다"는 것이 그의 입장이었다.

여기서 Kik 측은 npm, Inc.에 직접 연락함. 그리고 npm, Inc.는... Azer의 동의 없이 kik 패키지의 소유권을 Kik 메신저에게 이전했음.

이것에 분노한 Azer는 npm에 있던 자신의 모든 패키지 약 250개를 전부 삭제(unpublish)해버렸다. "이딴 플랫폼에 내 코드를 놔둘 이유가 없다"는 것.

문제는, 그 250개 중에 left-pad가 있었다는 것

Azer의 패키지 삭제
├── kik (분쟁의 원인)
├── left-pad ← 이게 폭탄이었음
├── ... (약 248개의 다른 패키지들)

Azer는 left-pad가 그렇게 널리 쓰이고 있는지 몰랐을 수도 있음. 아니, 알았어도 삭제했을 수도 있음. 어쨌든 결과는 대참사.


11줄의 코드: left-pad는 뭘 하는 함수인가

삭제된 left-pad의 전체 소스코드가 이것임:

javascript
module.exports = leftpad;

function leftpad(str, len, ch) {
  str = String(str);
  var i = -1;

  if (!ch && ch !== 0) ch = ' ';

  len = len - str.length;

  while (++i < len) {
    str = ch + str;
  }

  return str;
}
left-pad가 하는 일

문자열을 특정 길이로 맞추기 위해 왼쪽에 문자를 채우는 함수. 예를 들어 leftpad('5', 3, '0')'005'를 반환. 그게 전부임. 진짜로. 11줄.

이걸 보고 전 세계 개발자들이 한 생각:

"...진짜 이거 때문에 인터넷이 멈췄다고?"
"이거 그냥 직접 구현하면 3줄인데?"
"아니 이걸 왜 패키지로 설치함?"

맞는 말임. 근데 현실은 이랬다:

left-pad의 의존성 그래프 (2016년 3월 기준)

left-pad (11줄)
├── line-numbers가 의존
│   └── Babel이 의존
│       ├── React 빌드에 사용
│       ├── Angular 빌드에 사용
│       ├── Vue 빌드에 사용
│       └── 사실상 모든 모던 JS 프로젝트에 사용
├── string-width가 의존
│   └── 수백 개의 CLI 도구가 의존
└── 직접 의존하는 패키지만 수천 개

주간 다운로드 수: 2,486,696회 (삭제 직전 기준)
의존하는 패키지 수: 직접 + 간접 합쳐서 수만 개

폭발: 빌드가 안 돼

타임라인

2016-03-22 (UTC)

[시간 불명]  Azer, npm에서 약 250개 패키지 일괄 삭제
             (당시 npm은 unpublish 제한이 거의 없었음)

[수분 내]    CI/CD 파이프라인들이 하나둘 실패하기 시작
             npm install → left-pad 404 → 빌드 실패

[약 30분]    개발자 커뮤니티에서 "npm install이 안 된다" 보고 폭주
             GitHub Issues, StackOverflow, Twitter 동시 다발

[약 1시간]   원인 파악 — left-pad 삭제가 원인
             npm 팀 대응 시작

[약 2.5시간] npm, Inc.가 전례 없는 조치:
             삭제된 left-pad를 강제로 복원(un-unpublish)
             npm 역사상 처음으로 삭제된 패키지를 관리자가 복원한 사건

영향 받은 주요 프로젝트들

javascript
// 이날 전 세계 개발자들의 터미널에 떴던 에러
// React
npm ERR! 404 'left-pad' is not in the npm registry.
// → react-scripts가 babel에 의존, babel이 left-pad에 의존

// Babel
npm ERR! peer dep missing: left-pad@^1.0.0
// → 트랜스파일러 자체가 설치 불가

// Ember
npm ERR! 404 'left-pad' is not in the npm registry.

// 수천 개의 CI/CD 파이프라인
// Jenkins, Travis CI, CircleCI 등에서 동시에 빌드 실패
개발자들의 반응

"CI가 빨간색인데 내 코드는 하나도 안 바꿨는데?" — 전 세계에서 동시에 나온 말 "npm install이 안 되면 나는 아무것도 못 하는 개발자인 건가?" — 자아 성찰 "11줄짜리 코드 때문에 Google도 빌드가 안 된다고?" — 현실 직시


기술적 분석: 왜 이런 일이 가능했나

1. npm의 unpublish 정책 부재

2016년 당시 npm은 퍼블리시된 패키지를 누구든 자유롭게 삭제(unpublish)할 수 있었음. 아무런 제한 없이.

javascript
// 2016년 당시: 아무 제한 없이 삭제 가능
npm unpublish left-pad --force
// → 완료. 수백만 개 프로젝트의 의존성이 증발.

// 2016년 이후 변경된 정책:
// 1. 퍼블리시 후 72시간 이내에만 unpublish 가능
// 2. 다른 패키지가 의존하고 있으면 unpublish 불가
// 3. 삭제해도 24시간 동안 같은 이름+버전으로 재퍼블리시 불가
//    (supply chain attack 방지)
unpublish와 supply chain attack

패키지가 삭제되면 그 이름을 다른 사람이 차지할 수 있었음. 악의적인 공격자가 삭제된 유명 패키지명을 차지하고 악성 코드를 넣어서 퍼블리시하면? 기존에 그 패키지에 의존하던 모든 프로젝트가 악성 코드를 자동으로 설치하게 됨. 이걸 "dependency confusion" 또는 "supply chain attack"이라고 함.

2. 마이크로 패키지 문화의 문제

JavaScript/npm 생태계에는 "하나의 함수 = 하나의 패키지"라는 문화가 있었음. 이게 극단적으로 간 결과:

javascript
// npm에 실제로 있었던 (지금도 있는) 초소형 패키지들

// is-odd: 홀수 판별 (1줄)
module.exports = function isOdd(n) {
  return n % 2 === 1;
};
// 주간 다운로드: 500,000+
// 참고: is-even이라는 패키지도 있는데, is-odd에 의존함 ㅋㅋ

// is-positive: 양수 판별
module.exports = function isPositive(n) {
  return n > 0;
};

// is-number: 숫자 판별
// ... 이것도 별도 패키지

// is-even의 실제 구현 (실화)
var isOdd = require('is-odd');
module.exports = function isEven(n) {
  return !isOdd(n);
};
// is-odd를 import해서 반전시킨 것. 이것도 별도 패키지.
유닉스 철학의 극단적 해석

이런 마이크로 패키지 문화는 유닉스의 "하나의 프로그램은 하나의 일을 잘 하라"는 철학에서 왔다는 주장이 있음. 근데 유닉스에서 ls는 수천 줄이고 자체적으로 완결된 프로그램인 반면, is-odd는 1줄짜리 코드를 외부 의존성으로 가져오는 거임. 이건 철학의 적용이 아니라 오용.

3. 의존성 트리의 깊이 문제

전형적인 Node.js 프로젝트의 의존성 트리 (2016년 기준)

my-project (내 코드)
├── express (1)
│   ├── body-parser (2)
│   │   ├── bytes (3)
│   │   ├── content-type (3)
│   │   ├── raw-body (3)
│   │   │   ├── bytes (4)
│   │   │   └── iconv-lite (4)
│   │   │       └── ... (5+)
│   │   └── ... (4+)
│   ├── accepts (2)
│   │   ├── mime-types (3)
│   │   │   └── mime-db (4)
│   │   └── negotiator (3)
│   └── ... (2+, 수십 개)
├── babel (1)
│   ├── ... → left-pad (???)
│   └── 깊이 10+ 들어가서 left-pad에 도달
└── react (1)
    └── ... → babel → ... → left-pad (???)

node_modules 폴더 크기: 종종 수백 MB
패키지 수: 종종 1000개 이상
개발자가 직접 선택한 패키지: 10-20개
나머지: "이게 왜 깔렸지?" 수준의 간접 의존성
typescript
// 의존성 깊이를 분석하는 간단한 도구

interface DependencyNode {
  name: string;
  version: string;
  depth: number;
  dependedByCount: number; // 이 패키지에 의존하는 패키지 수
  linesOfCode: number;
}

function analyzeDependencyRisk(node: DependencyNode): string {
  // 리스크 매트릭스
  const highDependedBy = node.dependedByCount > 1000;
  const tinyPackage = node.linesOfCode < 50;
  const deepInTree = node.depth > 5;

  if (highDependedBy && tinyPackage) {
    return `CRITICAL: ${node.name}${node.dependedByCount}개 패키지가 의존하는데 ` +
           `${node.linesOfCode}줄밖에 안 됨. ` +
           `이걸 직접 구현하는 게 더 안전하지 않을까?`;
  }

  if (highDependedBy && deepInTree) {
    return `WARNING: ${node.name}은 의존성 트리 깊이 ${node.depth}에 있는데 ` +
           `${node.dependedByCount}개가 의존함. SPOF 위험.`;
  }

  return `OK: ${node.name}`;
}

// left-pad 분석 결과
analyzeDependencyRisk({
  name: 'left-pad',
  version: '1.1.3',
  depth: 7,
  dependedByCount: 3500,
  linesOfCode: 11,
});
// → "CRITICAL: left-pad은 3500개 패키지가 의존하는데 11줄밖에 안 됨.
//    이걸 직접 구현하는 게 더 안전하지 않을까?"

npm의 대응과 그 후

즉각적 대응

npm, Inc.는 전례 없는 조치를 취했음: 삭제된 패키지를 관리자 권한으로 강제 복원.

npm 측 대응 타임라인:

1. 장애 인지 → 즉시 원인 분석
2. left-pad가 원인임을 파악
3. Azer에게 연락 → 복원 요청
4. Azer 거부 (당연히)
5. npm 관리자 권한으로 강제 복원 (un-unpublish)
6. 생태계 정상화
7. 이후 unpublish 정책 전면 개정
관리자 강제 복원의 딜레마

이 결정도 논란이 됐음. "개발자가 자기 코드를 삭제할 권리가 없다는 건가?" vs. "수백만 프로젝트를 인질로 잡을 수는 없지 않나?" 두 입장 모두 일리가 있어서, npm의 역할이 단순한 패키지 호스팅이 아니라 공공 인프라에 가깝다는 인식이 생기기 시작했음.

정책 변경

typescript
// left-pad 사태 이후 변경된 npm 정책

interface NpmUnpublishPolicy {
  // Before (2016년 3월 이전)
  before: {
    timeLimit: null;             // 제한 없음
    dependencyCheck: false;      // 의존 패키지 확인 안 함
    adminOverride: false;        // 관리자 복원 전례 없음
  };

  // After (2016년 3월 이후)
  after: {
    timeLimit: '72 hours';       // 퍼블리시 후 72시간 내에만 삭제 가능
    dependencyCheck: true;       // 다른 패키지가 의존하면 삭제 불가
    adminOverride: true;         // 필요시 관리자가 개입 가능
    nameReuse: '24h cooldown';   // 삭제 후 같은 이름 재사용 24시간 쿨다운
  };
}

lock 파일의 부상

이 사건은 package-lock.json(npm 5에서 도입)과 yarn.lock(Yarn에서 도입)의 중요성을 각인시켰음.

json
// package.json — 버전 범위 지정 (느슨함)
{
  "dependencies": {
    "left-pad": "^1.0.0"  // 1.0.0 이상 2.0.0 미만의 최신 버전
  }
}
// → npm install 할 때마다 최신 버전을 레지스트리에서 가져옴
// → 레지스트리에서 삭제되면? 실패!

// package-lock.json — 정확한 버전 + 체크섬 고정
{
  "dependencies": {
    "left-pad": {
      "version": "1.1.3",
      "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.1.3.tgz",
      "integrity": "sha512-...정확한해시..."
    }
  }
}
// → 버전과 소스가 고정됨
// → 하지만 lock 파일이 있어도, 레지스트리에서 삭제되면 결국 실패
// → 결론: lock 파일 + private registry (또는 캐시)가 필요
typescript
// 의존성 안전을 위한 전략들

interface DependencyStrategy {
  // 1. Lock 파일
  lockFile: 'package-lock.json' | 'yarn.lock' | 'pnpm-lock.yaml' | 'bun.lockb';

  // 2. 프라이빗 레지스트리 (npm 미러)
  privateRegistry?: {
    url: string;
    // 외부 패키지를 캐싱하여 원본이 삭제되어도 영향 없음
    cacheExternalPackages: boolean;
  };

  // 3. Vendoring (의존성을 프로젝트에 직접 포함)
  vendoring?: {
    // node_modules를 git에 커밋 (극단적이지만 확실한 방법)
    commitNodeModules: boolean;
    // 또는 선별적으로 중요 패키지만 vendor
    vendoredPackages: string[];
  };

  // 4. 의존성 감사
  audit: {
    // npm audit 자동 실행
    runOnInstall: boolean;
    // 알려진 취약점이 있으면 빌드 실패
    failOnVulnerability: boolean;
    // 의존성 트리 깊이 제한
    maxDepth: number;
  };
}

String.prototype.padStart()의 탄생

이 사건의 가장 의미 있는 결과 중 하나. left-pad 사태 이후, JavaScript 표준(ECMAScript)에 String.prototype.padStart()String.prototype.padEnd()가 추가됨. ES2017(ES8) 스펙에 포함됨.

javascript
// 이제 이렇게 하면 됨. 패키지 설치 필요 없음.

// left-pad와 동일한 기능
'5'.padStart(3, '0');    // '005'
'hi'.padStart(10, ' ');  // '        hi'

// right-pad
'5'.padEnd(3, '0');      // '500'
'hi'.padEnd(10, '-');    // 'hi--------'

// 기본값은 공백
'5'.padStart(3);         // '  5'
11줄 코드가 남긴 것

left-pad의 기능이 JavaScript 언어 표준에 들어갔다는 건 상당히 아이러니한 상황. 11줄짜리 npm 패키지가 인터넷을 멈추고, 그 결과로 언어 표준이 바뀌었다. 어떤 의미에서 left-pad는 JavaScript 역사에서 가장 영향력 있는 11줄의 코드.


더 깊은 교훈: 의존성 철학

"Dependency Hell"은 어디서 시작되는가

typescript
// 의존성 추가 전 체크리스트

function shouldAddDependency(pkg: {
  name: string;
  linesOfCode: number;
  weeklyDownloads: number;
  lastUpdated: Date;
  maintainers: number;
  dependencies: number;
}): { decision: 'add' | 'implement' | 'reconsider'; reason: string } {

  // 규칙 1: 너무 작은 패키지는 직접 구현
  if (pkg.linesOfCode < 50) {
    return {
      decision: 'implement',
      reason: `${pkg.linesOfCode}줄짜리 코드를 외부 의존성으로? 직접 구현하자.`,
    };
  }

  // 규칙 2: 관리자가 1명인 패키지는 리스크
  if (pkg.maintainers <= 1) {
    return {
      decision: 'reconsider',
      reason: '관리자가 1명. 이 사람이 번아웃되거나 화나면 끝.',
    };
  }

  // 규칙 3: 마지막 업데이트가 2년 이상 전이면 주의
  const twoYearsAgo = new Date();
  twoYearsAgo.setFullYear(twoYearsAgo.getFullYear() - 2);
  if (pkg.lastUpdated < twoYearsAgo) {
    return {
      decision: 'reconsider',
      reason: '2년 이상 업데이트 없음. 유지보수 중단 가능성.',
    };
  }

  // 규칙 4: 의존성의 의존성이 너무 많으면 주의
  if (pkg.dependencies > 20) {
    return {
      decision: 'reconsider',
      reason: `이 패키지 하나가 ${pkg.dependencies}개의 의존성을 끌고 옴.`,
    };
  }

  return {
    decision: 'add',
    reason: '합리적인 의존성으로 판단됨.',
  };
}

// left-pad를 이 기준으로 평가하면:
shouldAddDependency({
  name: 'left-pad',
  linesOfCode: 11,
  weeklyDownloads: 2486696,
  lastUpdated: new Date('2016-03-15'),
  maintainers: 1,
  dependencies: 0,
});
// → { decision: 'implement', reason: '11줄짜리 코드를 외부 의존성으로? 직접 구현하자.' }

Vendor or Depend? — 의존성의 스펙트럼

typescript
// 의존성 전략의 스펙트럼

type DependencyApproach =
  | 'inline'          // 직접 구현 (1-50줄)
  | 'copy-paste'      // 코드 복사 (라이선스 확인 필요)
  | 'vendor'          // 소스를 프로젝트에 포함
  | 'lock-and-cache'  // lock 파일 + 캐시/미러
  | 'trust-registry'; // 그냥 npm install (가장 위험)

function recommendApproach(complexity: number, criticality: number): DependencyApproach {
  // complexity: 구현 복잡도 (1-10)
  // criticality: 프로젝트에서의 중요도 (1-10)

  if (complexity <= 2) return 'inline';        // 간단한 건 직접 구현
  if (complexity <= 4) return 'copy-paste';    // 좀 복잡하면 복사
  if (criticality >= 8) return 'vendor';       // 핵심이면 vendor
  if (criticality >= 5) return 'lock-and-cache'; // 중요하면 캐시
  return 'trust-registry';                     // 나머지는 신뢰
}

// left-pad: complexity=1, criticality=3 → 'inline'
// react: complexity=10, criticality=10 → 'vendor' (는 좀 과하고 lock-and-cache)
// lodash: complexity=8, criticality=6 → 'lock-and-cache'

교훈 정리

npm left-pad 사태에서 배우는 교훈

1. 작은 유틸리티는 직접 구현하라

  • 11줄짜리 코드를 외부 패키지로 가져오는 건 비용 대비 리스크가 너무 큼
  • padStart, isEven, isNumber 같은 건 직접 쓰자

2. 의존성 트리를 파악하라

  • npm ls --all로 실제로 뭐가 깔려 있는지 확인
  • 깊이 10에 숨어있는 11줄짜리 패키지가 전체를 죽일 수 있음

3. lock 파일은 필수, 캐시는 보험

  • package-lock.json / yarn.lock / bun.lockb 반드시 커밋
  • 프라이빗 레지스트리나 캐시 서버로 외부 의존성 미러링 고려

4. 단일 관리자 패키지는 리스크

  • 핵심 의존성의 관리자가 1명이면, 그 사람의 기분에 프로젝트가 좌우됨
  • 가능하면 다수의 관리자가 있거나, 재단/기업이 후원하는 패키지를 선택

5. 패키지 생태계는 공공 인프라다

  • npm, PyPI, Maven Central 등은 이제 인터넷 인프라의 일부
  • 개인의 자유(삭제 권한)와 공공의 이익(안정성) 사이의 균형이 필요

마치며

left-pad 사태는 JavaScript 생태계의 구조적 문제를 적나라하게 드러낸 사건임. 11줄짜리 코드에 수백만 프로젝트가 의존하고, 한 명의 개발자가 화나서 삭제하면 전 세계 빌드가 멈추는 구조. 이건 기술적 문제이면서 동시에 거버넌스 문제이기도 하다.

하지만 가장 뼈아픈 교훈은 이것임: 우리는 우리가 뭘 의존하고 있는지 모르고 있었다. npm install을 누르는 순간 수천 개의 패키지가 깔리고, 그 중 하나라도 사라지면 전부 무너지는 구조인데, 대부분의 개발자는 자기 프로젝트의 node_modules에 뭐가 들어있는지조차 모름.

지금 당장 node_modules 폴더 크기 확인해봐. 아마 node_modules가 블랙홀이라는 밈에 공감하게 될 거임.

다음 편에서는 코드 11줄보다 더 짧은 거 — 정규식 한 줄이 전 세계를 다운시킨 Cloudflare 사건을 다룬다.