
시맨틱 버저닝(SemVer): v1.0.0의 무게
개발자끼리의 무언의 약속. Major, Minor, Patch 숫자에 담긴 의미와 `npm install` 할 때 `^`와 `~`의 차이점 완벽 정리.

개발자끼리의 무언의 약속. Major, Minor, Patch 숫자에 담긴 의미와 `npm install` 할 때 `^`와 `~`의 차이점 완벽 정리.
내 서버는 왜 걸핏하면 뻗을까? OS가 한정된 메모리를 쪼개 쓰는 처절한 사투. 단편화(Fragmentation)와의 전쟁.

미로를 탈출하는 두 가지 방법. 넓게 퍼져나갈 것인가(BFS), 한 우물만 팔 것인가(DFS). 최단 경로는 누가 찾을까?

이름부터 빠릅니다. 피벗(Pivot)을 기준으로 나누고 또 나누는 분할 정복 알고리즘. 왜 최악엔 느린데도 가장 많이 쓰일까요?

매번 3-Way Handshake 하느라 지쳤나요? 한 번 맺은 인연(TCP 연결)을 소중히 유지하는 법. HTTP 최적화의 기본.

2023년 11월, 금요일 오후 4시 30분.
배포 30분 전, 저는 자신만만했습니다.
"이번 주 마지막 배포인데, 라이브러리 업데이트나 하고 가자."
npm update
터미널에 초록색 체크마크들이 쭉 뜨고, 모든 게 순조로워 보였습니다.
로컬에서 테스트 몇 개 돌려보고 "문제없네!" 하고는 배포 버튼을 눌렀습니다.
오후 5시 10분, 슬랙에 빨간 알림이 폭포처럼 쏟아지기 시작했습니다.
ERROR: Cannot find module 'createUser'
ERROR: login is not a function
ERROR: Uncaught TypeError at UserService.js:45
프로덕션이 완전히 멈췄습니다.
CEO가 직접 전화를 걸어왔고, 저는 식은땀을 흘리며 롤백을 시작했습니다. 문제는 "어느 버전으로 롤백해야 하는가?"였습니다.
package-lock.json을 열어보니:
{
"dependencies": {
"user-auth-lib": {
"version": "3.0.0" // ← 원래 2.8.1이었음
}
}
}
시니어 개발자가 제 화면을 보더니 한숨을 쉬었습니다.
"야, 이거 Major 버전 업데이트잖아. 2.x → 3.x는 Breaking Change야. 당연히 터지지."
저: "근데 npm update는 안전한 업데이트만 하는 거 아니에요?"
시니어: "package.json에 뭐라고 써있어?"
{
"dependencies": {
"user-auth-lib": "^2.8.1"
}
}
시니어: "이거 봐, 캐럿(^) 붙어있잖아. 이건 Minor 업데이트도 허용하는 거야. 근데 너 이 라이브러리 개발자가 SemVer 안 지킨 거 같은데? 3.0.0으로 올리면서 Breaking Change 안 써놨네."
그날 밤 11시까지 긴급 수정을 했고, 월요일에 CEO한테 1시간 동안 혼났습니다.
그때 깨달았습니다: "버전 숫자는 그냥 숫자가 아니구나. 이건 계약이고 약속이다."그날의 사고 이후, 저는 다음 질문들에 답을 찾아야 했습니다:
무엇보다, 제가 라이브러리를 만들게 되면 "어떻게 버전을 올려야 사용자들이 안심하고 쓸 수 있을까?"가 궁금했습니다.
"1.0.0에서 1.0.1로 올리든, 2.0.0으로 올리든 그냥 개발자 맘 아닌가?"
"npm이 알아서 안전한 버전만 설치하겠지?"
"package.json만 있으면 되는 거 아닌가?"
"v1.0.0-alpha.1이 뭐야? 알파? 이게 정식 버전인가?"
"^1.0.0이면 1.x.x는 다 안전하겠지?"
가장 큰 착각: "버전은 그냥 개발자가 적당히 올리는 숫자"롤백 작업을 하면서 시니어가 칠판에 그림을 그려줬습니다.
┌─────────────────────────────────────────────────────┐
│ MAJOR . MINOR . PATCH - PreRelease + Build │
│ 2 . 8 . 1 - beta.3 + 20231115 │
│ │ │ │ │ │ │
│ │ │ │ │ └─ 빌드 메타데이터
│ │ │ │ └─ 출시 전 테스트 버전
│ │ │ └─ 버그 수정 (하위 호환 O)
│ │ └─ 기능 추가 (하위 호환 O)
│ └─ API 파괴적 변경 (하위 호환 X)
└─────────────────────────────────────────────────────┘
시니어: "이게 Semantic Versioning의 공식이야. 각 숫자에는 명확한 의미가 있어."
"MAJOR 버전을 올린다 = 기존 코드가 작동 안 할 수 있다는 경고" "MINOR 버전을 올린다 = 새 기능 추가, 기존 코드는 안전" "PATCH 버전을 올린다 = 버그만 고침, 완전 안전"
저: "그럼 user-auth-lib가 2.8.1 → 3.0.0으로 올라간 건..."
시니어: "createUser() 함수 이름을 registerUser()로 바꿨거나, 파라미터 순서를 바꿨거나, 리턴 타입을 바꿨거나... 뭔가 API를 바꾼 거지. 그래서 MAJOR를 올린 거고."
그 순간 이해했습니다: "버전은 숫자가 아니라 약속이다. 계약서다."vMAJOR.MINOR.PATCH
─┬─── ─┬─── ─┬───
│ │ └─ PATCH: 버그 수정 (기존 API 변경 없음)
│ └────── MINOR: 기능 추가 (기존 API는 유지)
└─────────── MAJOR: API 변경 (기존 코드가 깨질 수 있음)
UserAPI// UserAPI v1.0.0
export function getUser(id) {
return fetch(`/api/users/${id}`)
.then(res => res.json());
}
export function createUser(data) {
return fetch('/api/users', {
method: 'POST',
body: JSON.stringify(data)
}).then(res => res.json());
}
사용자 코드:
import { getUser, createUser } from 'user-api';
getUser(123).then(user => console.log(user));
createUser({ name: 'John' }).then(user => console.log(user));
// UserAPI v1.0.1
// 버그 수정: ID가 null일 때 크래시 발생 → 에러 처리 추가
export function getUser(id) {
if (!id) {
return Promise.reject(new Error('ID is required')); // ← 수정
}
return fetch(`/api/users/${id}`)
.then(res => res.json());
}
export function createUser(data) {
if (!data || !data.name) {
return Promise.reject(new Error('Name is required')); // ← 수정
}
return fetch('/api/users', {
method: 'POST',
body: JSON.stringify(data)
}).then(res => res.json());
}
호환성: 기존 코드 그대로 작동 ✅
사용자는 아무것도 수정할 필요 없음.
// UserAPI v1.1.0
// 새 기능: 여러 사용자 조회 함수 추가
export function getUser(id) {
if (!id) {
return Promise.reject(new Error('ID is required'));
}
return fetch(`/api/users/${id}`)
.then(res => res.json());
}
export function createUser(data) {
if (!data || !data.name) {
return Promise.reject(new Error('Name is required'));
}
return fetch('/api/users', {
method: 'POST',
body: JSON.stringify(data)
}).then(res => res.json());
}
// ← 새로운 함수 추가! (기존 함수는 그대로)
export function getUsers(ids) {
return Promise.all(ids.map(id => getUser(id)));
}
export function deleteUser(id) {
return fetch(`/api/users/${id}`, { method: 'DELETE' })
.then(res => res.json());
}
호환성: 기존 코드 그대로 작동 ✅
사용자는 원한다면 새 함수를 쓸 수 있음. 안 써도 문제 없음.
// UserAPI v2.0.0
// Breaking Change: 함수 이름 변경, async/await로 전환
// getUser → fetchUser (이름 변경!)
export async function fetchUser(id) { // ← 이름 바뀜!
if (!id) throw new Error('ID is required');
const res = await fetch(`/api/users/${id}`);
return res.json();
}
// createUser → registerUser (이름 변경!)
export async function registerUser(data) { // ← 이름 바뀜!
if (!data || !data.name) throw new Error('Name is required');
const res = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify(data)
});
return res.json();
}
export async function fetchUsers(ids) {
return Promise.all(ids.map(id => fetchUser(id)));
}
export async function removeUser(id) { // ← deleteUser → removeUser
const res = await fetch(`/api/users/${id}`, { method: 'DELETE' });
return res.json();
}
호환성: 기존 코드 작동 안 함 ❌
// 기존 코드 (v1.x)
import { getUser, createUser } from 'user-api'; // ← 에러! 함수 없음
getUser(123).then(user => console.log(user)); // ← getUser is not defined
사용자는 코드를 수정해야 함:
// 수정된 코드 (v2.x)
import { fetchUser, registerUser } from 'user-api'; // ← 이름 변경
const user = await fetchUser(123); // ← async/await로 변경
console.log(user);
# package.json (배포 전날)
{
"dependencies": {
"axios": "^0.21.0" // ← 캐럿 붙음
}
}
# npm install (배포 당일 새 서버에서)
npm install
# 결과: package-lock.json
{
"axios": "1.0.0" // ← 0.x → 1.x로 MAJOR 점프!
}
문제: axios 0.x → 1.x는 Breaking Change였습니다.
axios.get() 응답 구조가 바뀜교훈: v0.x.y는 unstable로 간주되어 ^0.21.0이 1.0.0을 허용합니다.
2016년 3월, left-pad라는 11줄짜리 npm 패키지가 삭제되었습니다.
// left-pad 패키지 전체 코드 (11줄)
module.exports = leftpad;
function leftpad(str, len, ch) {
str = String(str);
ch = ch || ' ';
var i = -1;
len = len - str.length;
while (++i < len) {
str = ch + str;
}
return str;
}
하지만 이 패키지를 의존성으로 쓰는 패키지가 수천 개였고, 그 중 하나가 Babel이었습니다.
npm install
# 에러
npm ERR! 404 'left-pad' is not in the npm registry.
결과: React, Babel을 쓰는 전 세계 수만 개 프로젝트가 동시에 빌드 실패.
원인:
교훈: "작은 의존성 하나가 전체를 무너뜨릴 수 있다."
# package.json
{
"dependencies": {
"some-logging-lib": "~2.3.4" // ← 틸드: PATCH만 허용
}
}
# npm update
npm update
# 결과
{
"some-logging-lib": "2.3.5" // ← PATCH 업데이트
}
업데이트 후 프로덕션에서 로그가 폭증:
[INFO] User logged in
[DEBUG] Database query: SELECT * FROM users WHERE id = 123
[DEBUG] Query time: 45ms
[DEBUG] Result: {...}
[INFO] Session created
...
문제: v2.3.5에서 개발자가 "디버그 로그 추가"를 PATCH로 분류했지만, 실제로는 로그 양이 100배 증가해서 디스크가 꽉 참.
교훈: "PATCH도 side effect를 일으킬 수 있다. 완전히 안전한 건 아니다."
^ (Caret) - 가장 많이 씀{
"dependencies": {
"react": "^18.2.0"
}
}
규칙: "가장 왼쪽의 0이 아닌 숫자는 고정"
^18.2.0 허용 범위:
✅ 18.2.0
✅ 18.2.1 (PATCH)
✅ 18.3.0 (MINOR)
✅ 18.99.99 (MINOR)
❌ 19.0.0 (MAJOR)
^0.5.2 허용 범위 (주의!):
✅ 0.5.2
✅ 0.5.3 (PATCH)
❌ 0.6.0 (MINOR도 막음!)
❌ 1.0.0 (MAJOR)
^0.0.3 허용 범위 (더 엄격!):
✅ 0.0.3
❌ 0.0.4 (PATCH도 막음!)
왜 이렇게 설계됐나?
~ (Tilde) - 보수적{
"dependencies": {
"lodash": "~4.17.21"
}
}
규칙: "PATCH 버전만 업데이트"
~4.17.21 허용 범위:
✅ 4.17.21
✅ 4.17.22 (PATCH)
✅ 4.17.99 (PATCH)
❌ 4.18.0 (MINOR)
❌ 5.0.0 (MAJOR)
~1.2 허용 범위 (PATCH 생략):
✅ 1.2.0
✅ 1.2.1
❌ 1.3.0
~1 허용 범위 (MINOR, PATCH 생략):
✅ 1.0.0
✅ 1.1.0 (MINOR)
✅ 1.99.99 (MINOR)
❌ 2.0.0 (MAJOR)
{
"dependencies": {
"critical-lib": "3.5.2" // ← 정확히 3.5.2만
}
}
허용: 3.5.2만
사용 시기:
{
"dependencies": {
"pkg1": ">=1.2.0", // 1.2.0 이상
"pkg2": ">1.2.0", // 1.2.0 초과
"pkg3": "<=2.0.0", // 2.0.0 이하
"pkg4": "<2.0.0", // 2.0.0 미만
"pkg5": ">=1.0.0 <2.0.0" // 1.x 버전만
}
}
||{
"dependencies": {
"pkg": "^1.0.0 || ^2.0.0" // 1.x 또는 2.x
}
}
사용 예: 플러그인이 두 버전의 호스트를 지원할 때
{
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0" // React 17 또는 18 둘 다 OK
}
}
-{
"dependencies": {
"pkg": "1.2.0 - 1.5.0" // 1.2.0 ≤ version ≤ 1.5.0
}
}
동일 표현:
{
"dependencies": {
"pkg": ">=1.2.0 <=1.5.0"
}
}
{
"dependencies": {
"pkg1": "1.x", // >=1.0.0 <2.0.0 (1.x 버전)
"pkg2": "1.2.x", // >=1.2.0 <1.3.0 (1.2.x 버전)
"pkg3": "*" // >=0.0.0 (모든 버전) ← 위험!
}
}
| 표기법 | 안전성 | 업데이트 범위 | 추천 상황 |
|---|---|---|---|
^1.2.3 | 중간 | MINOR + PATCH | 대부분의 경우 (npm 기본) |
~1.2.3 | 높음 | PATCH만 | 안정성이 중요한 프로덕션 |
1.2.3 | 최고 | 업데이트 없음 | 레거시, 은행/의료 시스템 |
>=1.2.3 | 낮음 | 모든 상위 버전 | 개발 도구 |
* | 위험 | 모든 버전 | 절대 사용 금지 |
^0.x.y | 주의 | PATCH만 (0.x는 특수) | Unstable 패키지 |
v1.0.0-alpha.1
v1.0.0-alpha.2
v1.0.0-beta.1
v1.0.0-beta.2
v1.0.0-rc.1 (Release Candidate)
v1.0.0 (정식 출시)
의미:
alpha: 초기 개발 버전, 기능 불완전, 버그 많음beta: 기능 완성, 테스트 중, 버그 있을 수 있음rc (Release Candidate): 출시 후보, 거의 완성, 최종 테스트 중버전 우선순위:
1.0.0-alpha.1 < 1.0.0-alpha.2 < 1.0.0-beta.1 < 1.0.0-rc.1 < 1.0.0
v1.0.0+20231115
v1.0.0+build.123
v1.0.0-beta.1+exp.sha.5114f85
의미: 빌드 시간, 커밋 해시 등 메타데이터 (버전 비교에는 영향 없음)
1.0.0+build.1 == 1.0.0+build.2 (버전은 동일)
# 실제 React 버전 예시
18.0.0-alpha-e6be2d531-20211019
18.0.0-beta-24dd07bd2-20211208
18.0.0-rc.0
18.0.0-rc.1
18.0.0 # 정식 출시
18.0.1 # Patch
18.1.0 # Minor
18.2.0
# 정식 버전만 설치
npm install react # → 18.2.0
# Pre-release 포함
npm install react@next # → 19.0.0-rc.1 (최신 베타)
# 특정 Pre-release
npm install react@18.0.0-rc.1
# package.json에서 Pre-release 고정
{
"dependencies": {
"react": "18.0.0-rc.1" // ← 정확히 이 버전만
}
}
주의: Pre-release는 ^나 ~ 범위에 포함되지 않음!
{
"dependencies": {
"react": "^18.0.0" // ← 18.0.0-rc.1은 설치 안 됨!
}
}
팀원 A의 컴퓨터 (2023년 11월 1일):
npm install
# package.json
{
"dependencies": {
"axios": "^1.5.0"
}
}
# 설치된 버전
axios 1.5.0
팀원 B의 컴퓨터 (2023년 11월 15일):
npm install # 똑같은 package.json
# 설치된 버전
axios 1.6.0 # ← Minor 업데이트가 나왔음!
결과: A의 컴퓨터에서는 작동하는데 B의 컴퓨터에서는 버그 발생.
"내 컴퓨터에서는 되는데?" 사태.
# package-lock.json (팀 공유)
{
"name": "my-project",
"version": "1.0.0",
"lockfileVersion": 3,
"dependencies": {
"axios": {
"version": "1.5.0", # ← 정확한 버전 고정
"resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz",
"integrity": "sha512-..." # ← 체크섬으로 변조 검증
}
}
}
이제 팀원 B도:
npm install # ← package-lock.json을 읽음
# 설치된 버전
axios 1.5.0 # ← A와 동일한 버전!
| 도구 | lockfile | 특징 |
|---|---|---|
| npm | package-lock.json | JSON 형식, 가독성 낮음 |
| yarn | yarn.lock | YAML 형식, 가독성 높음 |
| pnpm | pnpm-lock.yaml | YAML, 심볼릭 링크 사용 |
# lockfile을 git에 커밋
git add package-lock.json
git commit -m "Lock dependencies"
# lockfile이 있으면 npm ci 사용 (CI/CD에서)
npm ci # ← package-lock.json 기준으로 정확히 설치
# lockfile을 .gitignore에 추가 (절대 금지!)
echo "package-lock.json" >> .gitignore # ❌
# package-lock.json 무시하고 설치
npm install --no-package-lock # ❌
# lockfile 수동 수정
vim package-lock.json # ❌ (npm이 관리함)
# 현재 버전: 1.2.3
# PATCH 올리기 (1.2.3 → 1.2.4)
npm version patch
# MINOR 올리기 (1.2.3 → 1.3.0)
npm version minor
# MAJOR 올리기 (1.2.3 → 2.0.0)
npm version major
동작:
version 필드 수정v1.2.4 메시지)v1.2.4)# 현재 버전: 1.2.3
# Pre-release로 올리기 (1.2.3 → 1.2.4-0)
npm version prerelease
# Pre-release 계속 올리기 (1.2.4-0 → 1.2.4-1)
npm version prerelease
# 정식 버전으로 (1.2.4-1 → 1.2.4)
npm version patch
# 현재 버전: 1.2.3
# Alpha 버전 (1.2.3 → 1.2.4-alpha.0)
npm version preminor --preid=alpha
# Beta 버전 (1.2.4-alpha.0 → 1.3.0-beta.0)
npm version preminor --preid=beta
# RC 버전 (1.3.0-beta.0 → 1.3.0-rc.0)
npm version prerelease --preid=rc
# git commit/tag 생성 안 함
npm version patch --no-git-tag-version
# 1. 기능 개발
git checkout -b feature/new-feature
# 2. 개발 완료, 커밋
git add .
git commit -m "feat: Add new feature"
# 3. main 브랜치로 돌아가기
git checkout main
git merge feature/new-feature
# 4. 버전 올리기 (자동 커밋/태그)
npm version minor # → v1.3.0
# 5. npm에 배포
npm publish
# 6. git에 푸시 (태그 포함)
git push origin main --tags
# 형식
<type>(<scope>): <subject>
# 예시
feat(auth): Add login function # → MINOR 버전 올림
fix(api): Fix null pointer bug # → PATCH 버전 올림
feat(api)!: Change API signature # → MAJOR 버전 올림
# 또는
feat(api): Change API signature
BREAKING CHANGE: API signature changed # → MAJOR 버전 올림
Type 종류:
feat: 새 기능 → MINORfix: 버그 수정 → PATCHdocs: 문서 변경 → 버전 안 올림style: 코드 스타일 변경 → 버전 안 올림refactor: 리팩토링 → PATCHperf: 성능 개선 → PATCHtest: 테스트 추가 → 버전 안 올림chore: 빌드 설정 등 → 버전 안 올림# 설치
npm install --save-dev standard-version
# package.json에 스크립트 추가
{
"scripts": {
"release": "standard-version"
}
}
# 사용
npm run release
동작:
feat 있으면 MINOR 올림, fix만 있으면 PATCH 올림BREAKING CHANGE 있으면 MAJOR 올림예시 CHANGELOG.md:
# Changelog
## [2.1.0] (2023-11-15)
### Features
- **auth**: Add OAuth login ([a3f2c1b](link-to-commit))
- **api**: Support pagination ([8d9e4f7](link-to-commit))
### Bug Fixes
- **api**: Fix null pointer in getUser ([5c6d8a2](link-to-commit))
## [2.0.0] (2023-11-01)
### BREAKING CHANGES
- **api**: Remove deprecated createUser function
Migration: Use registerUser instead
### Features
- **api**: Add registerUser function
# 설치
npm install --save-dev semantic-release
# .releaserc.json 설정
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer", // 커밋 분석
"@semantic-release/release-notes-generator", // 릴리스 노트 생성
"@semantic-release/changelog", // CHANGELOG 생성
"@semantic-release/npm", // npm 배포
"@semantic-release/git", // git commit/tag
"@semantic-release/github" // GitHub 릴리스 생성
]
}
# CI/CD에서 실행
npx semantic-release
CI/CD 워크플로우 (GitHub Actions):
# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm ci
- run: npm test
# 자동 버전 관리 & 배포
- run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
결과:
feat: 커밋 푸시 → CI가 자동으로 MINOR 버전 올리고 npm 배포v2.3.4 = MAJOR.MINOR.PATCH
장점:
단점:
사용 예: npm, pip, gems, 대부분의 라이브러리
Ubuntu: YY.MM (예: 22.04, 23.10)
PyPI: YYYY.MM.MICRO (예: 2023.11.1)
장점:
단점:
사용 예: Ubuntu, Windows (20H2, 21H1), pip 자체
Windows 10 21H2 (2021년 하반기)
Windows 11 22H2 (2022년 하반기)
Chrome 119.0.6045.123
│ │ │ └─ Patch (버그 수정)
│ │ └─────── Build (자동 빌드 번호)
│ └────────── Branch (개발 브랜치)
└────────────── Major (기능 변경)
4~6주마다 MAJOR 버전 올림 (API 변경 없어도).
iOS 17.1.1
│ │ └─ Patch (긴급 버그 수정)
│ └─── Minor (기능 추가)
└────── Major (메이저 업데이트, 연례 출시)
| 방식 | 형식 | 장점 | 단점 | 사용 예 |
|---|---|---|---|---|
| SemVer | MAJOR.MINOR.PATCH | API 호환성 명확 | 마케팅 불리 | npm, pip, gems |
| CalVer | YY.MM.MICRO | 시기 파악 쉬움 | 호환성 정보 없음 | Ubuntu, pip |
| Chrome식 | MAJOR.BRANCH.BUILD.PATCH | 빠른 출시 | 혼란스러움 | Chrome, Edge |
| iOS식 | MAJOR.MINOR.PATCH | 직관적 | SemVer와 다름 | iOS, macOS |
v1.5.3 → v1.5.4 (PATCH)
// v1.5.3
function login(username, password) {
return fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password })
});
}
// v1.5.4
function login(email, password) { // ← username → email 변경!
return fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password })
});
}
결과:
~1.5.3 (PATCH만 허용)으로 설정v2.3.0 → v2.4.0 (MINOR)
// v2.3.0
export function getUser(id) {
return fetch(`/api/users/${id}`)
.then(res => res.json());
}
// v2.4.0
export async function getUser(id) { // ← 동기 → 비동기 변경!
const res = await fetch(`/api/users/${id}`);
return res.json();
}
문제:
// 기존 사용자 코드 (v2.3.0)
getUser(123).then(user => console.log(user)); // ← 작동
// v2.4.0으로 업데이트 후
getUser(123).then(user => console.log(user)); // ← 여전히 작동하지만...
// 만약 이렇게 쓴 사용자가 있다면?
const user = getUser(123); // ← Promise 대신 undefined 반환
console.log(user.name); // ← TypeError!
올바른 방법: MAJOR 버전을 올려야 함 (2.3.0 → 3.0.0).
// v2.3.0
export function getUser(id) {
console.warn('getUser() is deprecated. Use fetchUser() instead.');
return fetchUser(id);
}
export function fetchUser(id) { // ← 새 함수 추가 (MINOR)
return fetch(`/api/users/${id}`)
.then(res => res.json());
}
타임라인:
fetchUser() 추가, getUser()는 유지 (MINOR)getUser() deprecated 경고 (MINOR)getUser() 제거 (MAJOR)# v3.0.0 Migration Guide
## Breaking Changes
### 1. `getUser()` → `fetchUser()`
**Before (v2.x)**:
\`\`\`javascript
import { getUser } from 'my-lib';
getUser(123).then(user => console.log(user));
\`\`\`
**After (v3.x)**:
\`\`\`javascript
import { fetchUser } from 'my-lib';
const user = await fetchUser(123);
console.log(user);
\`\`\`
**Codemod (자동 마이그레이션)**:
\`\`\`bash
npx @my-lib/codemod v2-to-v3
\`\`\`
### 2. Login API signature change
**Before**:
\`\`\`javascript
login(username, password)
\`\`\`
**After**:
\`\`\`javascript
login(email, password)
\`\`\`
* 또는 latest 사용{
"dependencies": {
"some-lib": "*" // ❌ 위험!
}
}
문제: MAJOR 업데이트가 자동으로 설치됨.
해결:
{
"dependencies": {
"some-lib": "^2.3.0" // ✅ MAJOR는 막음
}
}
{
"devDependencies": {
"eslint": "8.56.0", // ← 고정 버전
"prettier": "3.1.0"
}
}
문제: 개발 도구는 자주 업데이트되는데 수동으로 관리해야 함.
해결:
{
"devDependencies": {
"eslint": "^8.56.0", // ✅ Minor 업데이트 허용
"prettier": "^3.1.0"
}
}
{
"peerDependencies": {
"react": "18.2.0" // ❌ 정확히 18.2.0만 허용
}
}
문제: 사용자가 React 18.3.0을 쓰면 경고 발생.
해결:
{
"peerDependencies": {
"react": "^18.0.0" // ✅ React 18.x 모두 허용
// 또는
"react": "^17.0.0 || ^18.0.0" // ✅ 17, 18 둘 다 지원
}
}
# .gitignore
node_modules/
package-lock.json # ❌ 절대 금지!
문제: 팀원마다 다른 버전 설치 → "내 컴퓨터에서는 되는데?" 사태.
해결:
# .gitignore
node_modules/
# package-lock.json은 절대 추가하지 말 것!
# 설치
npm install -g npm-check-updates
# 업데이트 가능 버전 확인
ncu
Checking package.json
[====================] 12/12 100%
axios ^1.5.0 → ^1.6.5 (Minor)
react ^18.2.0 → ^18.2.0 (Up to date)
lodash ^4.17.21 → ^4.17.21 (Up to date)
# MAJOR 업데이트 포함
ncu --target latest
react ^18.2.0 → ^19.0.0 (MAJOR! 주의!)
# package.json 자동 업데이트
ncu -u
# 특정 패키지만
ncu axios
npm outdated
Package Current Wanted Latest Location
axios 1.5.0 1.6.5 1.6.5 my-project
react 18.2.0 18.2.0 19.0.0 my-project
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
reviewers:
- "my-team"
labels:
- "dependencies"
동작: 매주 자동으로 PR 생성, 업데이트 가능한 의존성 표시.
Dependabot보다 강력한 대안:
// renovate.json
{
"extends": ["config:base"],
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch"],
"automerge": true // Minor/Patch는 자동 머지
},
{
"matchUpdateTypes": ["major"],
"labels": ["major-update"],
"reviewers": ["team-lead"] // Major는 리뷰 필요
}
]
}
v0.x.y = "Initial development. Anything MAY change at any time."
의미: MAJOR가 0이면 unstable로 간주됨.
React v0.14.0 → v0.15.0 (MINOR 업데이트)
// v0.14.0
React.render(<App />, document.body);
// v0.15.0
ReactDOM.render(<App />, document.body); // ← Breaking Change!
문제: MINOR 업데이트인데 API가 완전히 바뀜!
{
"dependencies": {
"stable-lib": "^1.5.0", // 1.x 모두 허용
"unstable-lib": "^0.5.0" // 0.5.x만 허용 (PATCH만!)
}
}
이유: v0.x.y는 MINOR도 Breaking일 수 있어서 PATCH만 허용.
{
"dependencies": {
"unstable-lib": "0.5.2" // ← 고정 버전
// 또는
"unstable-lib": "~0.5.2" // ← PATCH만 (명시적)
}
}
변경사항이 있는가?
├─ YES
│ ├─ 기존 코드가 깨지는가? (Breaking Change)
│ │ ├─ YES → MAJOR 버전 올림 (1.0.0 → 2.0.0)
│ │ └─ NO
│ │ ├─ 새 기능 추가?
│ │ │ ├─ YES → MINOR 버전 올림 (1.0.0 → 1.1.0)
│ │ │ └─ NO
│ │ │ ├─ 버그 수정?
│ │ │ │ ├─ YES → PATCH 버전 올림 (1.0.0 → 1.0.1)
│ │ │ │ └─ NO → 버전 안 올림 (문서, 테스트만 변경)
└─ NO → 버전 안 올림
다음 중 하나라도 해당되면 MAJOR:
{
"dependencies": {
// 프레임워크 (중요): 고정 또는 틸드
"react": "18.2.0",
"vue": "~3.3.0",
// 유틸 라이브러리: 캐럿
"lodash": "^4.17.21",
"axios": "^1.6.0",
// v0.x.y (unstable): 고정
"new-experimental-lib": "0.5.2",
// 플러그인: OR 범위 (여러 호스트 버전 지원)
"eslint-plugin-react": "^7.0.0"
},
"devDependencies": {
// 개발 도구: 캐럿 (자유롭게 업데이트)
"eslint": "^8.56.0",
"prettier": "^3.1.0",
"jest": "^29.7.0"
},
"peerDependencies": {
// 호스트 라이브러리: 넓은 범위
"react": "^17.0.0 || ^18.0.0"
}
}
처음 버전 관리를 공부하기 시작했을 때, 저는 "그냥 숫자 아냐?"라고 생각했습니다.
프로덕션을 날려먹고 나서야 깨달았습니다.
"버전은 숫자가 아니라 개발자 간의 신뢰 프로토콜이다."v2.3.4에서 v2.3.5로 올라가는 건 단순한 증가가 아닙니다. "이 업데이트는 안전합니다. 당신의 코드는 그대로 작동할 겁니다"라는 약속입니다.
v2.3.4에서 v3.0.0으로 올라가는 건 "주의하세요. 코드를 수정해야 할 수 있습니다"라는 경고입니다.
제가 배운 교훈:
제가 이제 라이브러리를 만든다면, SemVer를 철저히 지킬 것입니다.
왜냐하면 제가 사용자의 금요일 밤을 망치고 싶지 않기 때문입니다.
"신뢰는 천천히 쌓이지만, 한 번의 Breaking Change로 무너진다."여러분도 라이브러리를 만든다면, 버전 번호를 신중하게 올려주세요.
숫자 하나하나에 책임감을 담아주세요.
그것이 개발자로서 지켜야 할 최소한의 예의입니다.