$ yh.log
[스터디] npm deep dive - 1장 npm과 유의적 버전

[스터디] npm deep dive - 1장 npm과 유의적 버전

npmSemVer패키지 관리JavaScript

작성자 : 오예환 | 작성일 : 2026-01-20 | 수정일 : 2026-01-20 | 조회수 :

1장에서 배울 내용

  • npm이란?: Node.js 패키지 관리 도구
  • npm의 배경과 역사, npm이 제공하는 기능
  • npm을 배우면 JavaScript 패키지 관리자 전반을 이해하는 데 도움이 됨

유의적 버전 (Semantic Versioning)

  • 유의적 버전 = 의미가 있는 버전
  • 패키지 관리에서 가장 중요한 역할을 하는 것이 바로 버전
  • 버전에 대한 규약을 유의적 버전(SemVer)에서 정의하고 있음
  • 유의적 버전에서 발생할 수 있는 문제점 파악
  • 오픈소스 생태계의 일원으로서 가져야 할 마음가짐

npm의 정의

npm = Node Package Manager

하지만 npm의 전신은 다양한 bash 유틸리티인 pm이며, pm = pkgmakeinst의 약자이다.

💡
bash란?

**Bash (Bourne Again SHell)**는 Unix/Linux 시스템에서 사용하는 명령어 해석기(셸)이다. 터미널에서 명령어를 입력하면 bash가 이를 해석하여 운영체제에 전달한다. bash 유틸리티란 이러한 셸 환경에서 실행되는 작은 프로그램들을 의미하며, npm의 전신인 pm도 이러한 bash 유틸리티 중 하나였다.

npm은 pm을 이어받아 진화시킨 하나의 프로젝트이다.

굳이 약자로 소개한다면 node pm 또는 node pkgmakeinst라고 할 수 있다.


npm의 역사와 성장

초기: Node.js 생태계 전용

npm이 처음 만들어진 시점부터 회사를 설립한 무렵까지는 대부분 Node.js 생태계에서 동작하는 코드만 업로드되어 있었다.

💡
Node.js 생태계에서 동작하는 코드란?

서버 사이드에서 실행되는 JavaScript 코드를 의미한다. 예를 들어:

  • express: 웹 서버 프레임워크
  • fs, path: 파일 시스템 접근
  • http: HTTP 서버 생성
  • 데이터베이스 연결 라이브러리 (mongoose, mysql 등)

이 시기에는 브라우저에서 실행되는 프론트엔드 코드(jQuery, Bootstrap 등)는 npm이 아닌 CDN에서 직접 다운로드하여 사용하는 것이 일반적이었다.

전환점: React의 등장

페이스북이 React를 npm에 배포한 것을 필두로, 프론트엔드 라이브러리들도 점차 npm으로 이동하기 시작했다.

CDN 방식의 문제점:

  • 업로드/다운로드 시 발생하는 비용
  • 보안 문제 (중간자 공격, 변조 위험)
  • 버전 관리의 어려움

npm이 해결한 것:

  • 무료로 패키지 호스팅
  • 버전별 관리 및 의존성 자동 해결
  • package.json을 통한 명확한 버전 명시
💡
제삼자 스크립트(Third-party Script) 방식이란?

HTML에서 외부 CDN의 JavaScript 파일을 직접 불러오는 방식이다:

<!-- 과거: CDN에서 직접 불러오기 (제삼자 스크립트 방식) -->
<script src="https://cdn.example.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.example.com/react-17.0.2.min.js"></script>
// 현재: npm으로 설치 후 import
import $ from "jquery";
import React from "react";

제삼자 스크립트 방식은 버전 관리가 어렵고, CDN 서버 장애 시 사이트가 동작하지 않는 문제가 있었다.

현재: npm의 대세화

현재는 제삼자 스크립트 방식으로 사용하는 JavaScript 패키지는 거의 찾아볼 수 없을 정도로 npm이 표준이 되었다.

2020년, 마이크로소프트가 인수한 GitHub가 npm을 인수하면서 더욱 안정적인 생태계가 구축되었다.


npm의 주요 기능

  • 패키지 설치 및 관리: 의존성 자동 해결, 버전 관리
  • 패키지 배포 및 공유: npmjs.com을 통한 전 세계 개발자와 공유
  • 스크립트 실행: package.json의 scripts를 통한 빌드/테스트 자동화
  • 패키지 레지스트리: npmjs.com에서 패키지 정보(개요, 코드, 의존성, 다운로드 수 등) 확인 가능

유용한 npm 관련 사이트

사이트용도링크
Bundlephobia패키지 번들 크기 확인bundlephobia.com
npm trends패키지 다운로드 횟수 비교, 유사 패키지 탐색npmtrends.com
unpkgnpm에 업로드된 파일 직접 확인 및 CDN 제공unpkg.com
Snyk패키지 보안 취약점 확인snyk.io

유의적 버전 (Semantic Versioning)

macOS, iOS, 게임, 안드로이드 등 모든 소프트웨어에는 버전이 존재한다. 이 버전은 **유의적 버전(SemVer)**이라 불리는 규칙에 의해 관리되며, 언제 어떤 버전 번호를 올리고 어떻게 작성해야 하는지 구체적으로 정의되어 있다.

등장 배경

오픈소스 패키지는 계속해서 진화하고 발전한다. 하지만 새로운 버전에서 무엇이 달라졌는지 일일이 추적하기란 거의 불가능하다.

버전 업데이트에 대한 두 가지 접근:

접근 방식장점단점
보수적 (업데이트 최소화)안정성 유지새로운 기능, 보안 패치 누락
진보적 (항상 최신 버전)최신 기능 활용호환성 문제 발생 가능

이러한 혼란을 제어하기 위해 만들어진 것이 바로 **유의적 버전(Semantic Versioning)**이다.

핵심 요약

MAJOR.MINOR.PATCH (주.부.수)
예: 2.1.3
버전올리는 시점예시
주(Major)기존 버전과 호환되지 않는 API 변경1.x.x2.0.0
부(Minor)기존 버전과 호환되면서 새 기능 추가1.1.x1.2.0
수(Patch)기존 버전과 호환되면서 버그 수정1.1.11.1.2
💡
문서 변경 시 버전은?

문서나 README만 변경한 경우, 코드 동작에 영향이 없으므로 버전을 올리지 않아도 된다. 단, 프로젝트 정책에 따라 수(Patch) 버전을 올리는 경우도 있다.


유의적 버전 명세 11가지


유의적 버전 검증하기

정규 표현식으로 문자열이 유의적 버전 문법에 맞는지 검증할 수 있지만, 일반적으로 semver 패키지를 사용하는 것이 권장된다.

npm install semver
const semver = require("semver");
 
semver.valid("1.2.3"); // '1.2.3'
semver.valid("a.b.c"); // null
semver.gt("1.2.3", "1.2.2"); // true (greater than)
semver.satisfies("1.2.3", "^1.0.0"); // true

버전 범위 지정: 캐럿(^)과 틸드(~)

package.json에서 의존성 버전을 지정할 때 사용하는 기호들이다.

기호의미예시허용 범위
^ (캐럿)Minor 버전까지 자동 업데이트^1.2.3>=1.2.3 <2.0.0
~ (틸드)Patch 버전까지 자동 업데이트~1.2.3>=1.2.3 <1.3.0
없음정확히 해당 버전만1.2.31.2.3
{
  "dependencies": {
    "lodash": "^4.17.21", // 4.x.x 범위에서 최신 버전
    "moment": "~2.29.4", // 2.29.x 범위에서 최신 버전
    "axios": "1.6.0" // 정확히 1.6.0만
  }
}
⚠️
주의

^~는 편리하지만, 패키지 제작자가 유의적 버전을 지키지 않으면 예기치 않은 Breaking Change가 발생할 수 있다.


npm 생태계의 유의적 버전 사건들

유의적 버전 규칙이 반드시 안전하거나 완벽한 해결책이 아니라는 점을 보여주는 사례들이 있다. 수많은 패키지가 얽혀있는 대규모 의존성 체계에서는 유의적 버전 관리의 허점이 드러난다.

left-pad 사건 (2016)

11줄짜리 간단한 문자열 패딩 라이브러리가 npm에서 삭제되면서, 이에 의존하던 Babel, React 등 수천 개의 프로젝트가 빌드 실패를 겪었다.

npm의 대응:

  • 다른 패키지가 의존하는 경우 패키지 삭제를 어렵게 변경
  • security-holder 패키지 도입: 버려진 유명 패키지의 이름을 다른 사람이 점유하여 악성 코드를 올리는 것을 방지

everything 패키지 사건

left-pad 사건 이후 "다른 패키지가 의존하면 삭제할 수 없다"는 규칙이 생기자, 이를 악용한 사건이 발생했다.

누군가 npm에 존재하는 모든 패키지를 의존성으로 두는 패키지를 만들어, npm의 대부분의 패키지가 삭제되지 않는 상황이 발생했다.

is-promise 사건 (2020)

단 2줄짜리 패키지가 유의적 버전 규칙을 지키지 않고 ES Module 전용으로 변경하면서, 이에 의존하던 create-react-app 등 수많은 프로젝트에서 에러가 발생했다.

// is-promise의 전체 코드 (2줄)
module.exports = isPromise;
function isPromise(obj) {
  return (
    !!obj &&
    (typeof obj === "object" || typeof obj === "function") &&
    typeof obj.then === "function"
  );
}

colors.js / faker.js 사건 (2022)

오픈소스 메인테이너가 의도적으로 악성 코드를 삽입한 버전을 배포. 무한 루프와 이상한 문자열을 출력하도록 변경되어 이에 의존하던 프로젝트들이 피해를 입었다.

event-stream 사건 (2018)

메인테이너가 유지보수 권한을 타인에게 넘겼는데, 새 메인테이너가 암호화폐 지갑을 탈취하는 악성 코드를 삽입한 사건.


여러 버전 라인에서 버그 수정하기

💡
Q: 1.2.2와 1.3.7 모두에 같은 버그가 있다면?

A: 각 버전 라인에서 독립적으로 수정한다.

현재 버전버그 수정 후
1.2.21.2.3
1.3.71.3.8

부(Minor) 버전이 올라간 순간부터 독립적인 버전 라인으로 관리된다. 1.2.x 사용자와 1.3.x 사용자 모두에게 버그 수정을 제공하려면 각각 패치 버전을 올려야 한다.

실제로 Node.js, React 등 대형 프로젝트는 여러 버전 라인을 동시에 유지보수한다.


유의적 버전과 npm 사용 시 주의점

"위험은 스스로가 무엇을 하고 있는지 모르기 때문에 발생한다."

어떤 위험을 가지고 있는지 충분히 아는 것이 중요하다.

1. 유의적 버전은 "규약"일 뿐이다

  • CHANGELOG.md릴리스 노트를 반드시 확인하자
  • 일부 패키지는 유의적 버전을 준수하지 않을 수 있다
  • 방어적으로 ^, ~ 대신 고정 버전을 사용하는 것도 방법
// 방어적 접근
{
  "dependencies": {
    "some-package": "1.2.3" // 고정 버전
  }
}

2. 무조건 설치하는 것이 능사가 아니다

오픈소스의 장점은 "바퀴를 재발명하지 않아도 된다"는 것이지만, 패키지를 설치한다는 것은 위험성을 외부에 둔다는 의미이기도 하다.

패키지 설치 전 체크리스트:

  1. 정말로 대체 불가능한 패키지인가?
  2. 사용할 수 있을 만큼 충분히 검증되었는가?
  3. 타당한 dependencies를 가지고 있는가?

3. 락파일 변경에 주의를 기울이기

package-lock.json 변경은 실제로 설치되는 버전의 변경을 의미한다.

# 락파일이 변경될 수 있음
npm install
 
# 락파일을 기준으로 정확히 설치 (CI/CD 권장)
npm ci
명령어동작
npm installpackage.json 기준, 락파일 업데이트 가능
npm cipackage-lock.json 기준, 정확히 일치하는 버전 설치
💡
npm ci를 사용해야 하는 경우
  • CI/CD 파이프라인 - 프로덕션 배포 - 팀원 간 동일한 환경 보장이 필요할 때

4. 보안 취약점에 귀 기울이기

# 취약점 검사
npm audit
 
# 자동 수정 시도
npm audit fix

정기적으로 의존성의 보안 취약점을 확인하고, Snyk이나 GitHub Dependabot 같은 도구를 활용하자.



[생각해보기]

실제 프로덕트에서 버저닝이 필수가 아닌 이유

유의적 버전은 **패키지(라이브러리)**를 배포할 때 필수적이지만, 실제 서비스 프로덕트(웹 애플리케이션, 모바일 앱 등)에서는 반드시 필요하지 않다.

패키지 vs 프로덕트의 차이

구분패키지 (라이브러리)프로덕트 (서비스)
사용자다른 개발자최종 사용자 (End User)
의존 관계다른 프로젝트가 의존의존하는 프로젝트 없음
버전 선택사용자가 버전 선택항상 최신 버전만 제공
호환성 고려이전 버전과의 호환 필수필요 없음

프로덕트에서 버저닝이 필요 없는 이유

1. 의존하는 외부 프로젝트가 없다

// 패키지: 다른 프로젝트가 이 버전에 의존
"dependencies": {
  "my-library": "^1.2.3"  // 버전 중요!
}
 
// 프로덕트: 아무도 의존하지 않음
// package.json의 version은 내부 관리용일 뿐

2. 사용자는 항상 최신 버전만 사용한다

웹 서비스나 SaaS는 서버에 배포되면 모든 사용자가 동시에 최신 버전을 사용한다. "1.2.3 버전을 쓰고 싶어요"라고 요청할 수 없다.

3. Breaking Change 개념이 다르다

  • 패키지: API 변경 시 기존 코드가 동작하지 않을 수 있음
  • 프로덕트: UI/UX 변경은 사용자 교육으로 해결, 코드 호환성 문제 없음

대신 사용하는 방식

방식설명예시
Git 태그배포 시점 기록v2024-01-20, release-42
빌드 번호CI/CD 자동 생성build-1234, sha-abc123
날짜 기반배포 날짜로 구분20260120.1
# Git 태그로 배포 관리
git tag -a release-2026-01-20 -m "Production release"
git push origin release-2026-01-20
💡
결론

npm에 배포하는 패키지라면 유의적 버전은 필수다. 하지만 웹 서비스나 내부 프로덕트라면 package.jsonversion 필드는 형식적인 것일 뿐, 실제 배포 관리는 Git 태그나 CI/CD 빌드 번호로 하는 것이 일반적이다.


React Native는 왜 아직도 0.x 버전일까?

React Native는 2015년 출시 이후 10년이 지났지만, 여전히 0.7x 버전대를 유지하고 있다. 수많은 프로덕션 앱에서 사용되는데, 왜 1.0.0을 선언하지 않을까?

유의적 버전 명세를 다시 보자

명세 4번: 주 버전 0은 초기 개발을 위해 쓴다. 아무 때나 마음대로 바꿀 수 있다. 이 공개 API는 안정판으로 보지 않는 게 좋다.

0.x 버전은 "아직 API가 안정화되지 않았다"는 의미다. 즉, Breaking Change가 언제든 발생할 수 있다는 선언이다.

React Native가 0.x를 유지하는 이유

1. API가 아직 안정화되지 않았다

React Native는 iOS/Android 네이티브 플랫폼과 연동되는 복잡한 프레임워크다. 플랫폼 변경에 따라 API가 자주 바뀔 수밖에 없다.

// 과거 버전
import { ListView } from "react-native"; // Deprecated
 
// 현재 버전
import { FlatList } from "react-native"; // 대체됨

2. Breaking Change에 대한 자유도 확보

1.0.0을 선언하면 유의적 버전 규칙상 Breaking Change 시 주 버전을 올려야 한다. 0.x 상태에서는 Minor 버전만 올려도 Breaking Change를 포함할 수 있다.

# 0.x에서는 이것이 가능
0.72.0 0.73.0  # Breaking Change 포함 가능
 
# 1.x라면 이렇게 해야 함
1.0.0 2.0.0   # Breaking Change

3. 커뮤니티 기대치 관리

"0.x = 불안정"이라는 인식을 통해, 사용자들이 업그레이드 시 주의가 필요하다는 것을 자연스럽게 받아들이게 한다.

비슷한 사례들

프로젝트버전특징
React Native0.7x10년째 0.x 유지
Electron30+빠르게 주 버전 증가
Kubernetes1.x안정화 후 1.0 선언

결론

0.x 버전이라고 해서 프로덕션에서 사용할 수 없는 것은 아니다. 단지 "API가 언제든 바뀔 수 있으니 업그레이드 시 주의하라"는 메시지일 뿐이다.

React Native 팀은 의도적으로 0.x를 유지하며, 이는 빠른 발전과 Breaking Change의 자유를 위한 전략적 선택이다.

⚠️
0.x 패키지 사용 시 주의

0.x 버전 패키지를 사용할 때는 Minor 버전 업데이트에도 Breaking Change가 있을 수 있다는 점을 인지하고, 업그레이드 전 반드시 릴리스 노트를 확인하자.