npm install vs npm ci
이런 경험 있으신가요?
여러분도 이런 상황을 겪어본 적 있나요? CI/CD 파이프라인에서 의존성 설치만 5분이 넘게 걸리거나, 로컬에서는 완벽하게 작동하던 앱이 프로덕션 배포 후 갑자기 에러를 내는 경우.
저도 최근에 정확히 이 문제를 만났습니다. GitHub Actions에서 npm install을 사용했는데, 빌드 시간이 130초나 걸렸어요. 더 큰 문제는 팀원마다 미묘하게 다른 버전의 패키지가 설치되어서, “내 컴퓨터에서는 되는데요” 현상이 발생했습니다.
바로 그때 npm ci를 알게 되었습니다. 같은 프로젝트에서 40초로 단축되었고, 모든 환경에서 정확히 동일한 의존성이 설치되었습니다.
npm install과 npm ci, 무엇이 다른가요?
둘 다 의존성을 설치하지만, 동작 방식과 철학이 완전히 다릅니다.
npm install: “유연한 개발자”
npm install
npm install은 개발 편의성에 초점을 맞춥니다.
- package.json을 읽고 의존성 목록 생성
- package-lock.json을 참조하여 설치할 버전 결정
- 의존성이 lock 파일에 없으면 새로 추가
- 기존 node_modules 유지하며 필요한 부분만 업데이트
- package-lock.json 수정 가능
npm ci: “엄격한 빌드 머신”
npm ci
npm ci는 일관성과 속도에 초점을 맞춥니다.
- package-lock.json에서만 직접 설치 (의존성 해결 과정 생략)
- package.json은 검증용으로만 사용 (불일치 시 에러)
- 기존 node_modules를 완전히 삭제 후 새로 설치
- 어떤 파일도 수정하지 않음 (frozen install)
- 전체 프로젝트만 설치 가능 (개별 패키지 불가)
“ci”의 의미: “clean install”의 약자입니다. 깨끗한 상태에서 설치한다는 의미죠.
핵심 차이점 한눈에 보기
| 구분 | npm install | npm ci |
|---|---|---|
| 속도 (캐시 없음) | 느림 (~130초) | 빠름 (~40초, 2배 이상 빠름) |
| 속도 (캐시 있음) | 매우 빠름 (~2초) | 보통 (~16초) |
| package-lock.json | 선택사항, 수정 가능 | 필수, 읽기 전용 |
| package.json | 읽고 수정 가능 | 검증용으로만 사용 |
| node_modules 처리 | 유지하며 업데이트 | 항상 삭제 후 재설치 |
| 개별 패키지 설치 | ✅ 가능 (npm install lodash) |
❌ 불가능 |
| 의존성 해결 | 매번 해결 | 생략 (lock 파일 사용) |
| 결정론적 설치 | ❌ 환경마다 다를 수 있음 | ✅ 항상 동일 |
| 용도 | 로컬 개발 | CI/CD, 프로덕션 |
왜 이런 차이가 생겼을까요?
npm install의 딜레마
// package.json
{
"dependencies": {
"express": "^4.18.0" // ^ = 4.18.0 이상, 5.0.0 미만
}
}
^4.18.0이라는 범위는:
- 2023년에 설치하면 →
4.18.2 - 2024년에 설치하면 →
4.18.7 - 2025년에 설치하면 →
4.19.0(새 버전 출시됨)
문제: 같은 package.json이지만, 설치 시점에 따라 다른 버전이 설치됩니다!
npm ci의 해결책
// package-lock.json (일부)
{
"dependencies": {
"express": {
"version": "4.18.2", // 정확한 버전 명시
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
"integrity": "sha512-..." // 무결성 해시
}
}
}
npm ci는 lock 파일의 정확한 버전을 설치합니다. 언제 어디서 설치하든 항상 4.18.2가 설치됩니다!
실전 예제
예제 1: 로컬 개발 워크플로우
시나리오: 프로젝트를 처음 클론했을 때
# ❌ 옛날 방식
git clone https://github.com/my-team/project.git
cd project
npm install # package.json 읽고, lock 파일 업데이트할 수도 있음
# ✅ 추천 방식
git clone https://github.com/my-team/project.git
cd project
npm ci # lock 파일의 정확한 버전 설치, 파일 수정 없음
왜 npm ci를 사용하나요?
- 팀원이 커밋한 lock 파일을 정확히 재현
- package.json과 lock 파일 불일치를 조기에 발견
- 깨끗한 상태로 시작
예제 2: 새 패키지 추가
시나리오: 프로젝트에 lodash 추가
# ✅ npm install 사용 (개별 패키지 설치는 npm install만 가능)
npm install lodash
# package.json과 package-lock.json이 자동으로 업데이트됨
git add package.json package-lock.json
git commit -m "feat: add lodash dependency"
핵심: 개별 패키지를 추가/제거할 때는 npm install만 가능합니다.
예제 3: 의존성 업데이트 후
시나리오: 팀원이 package-lock.json을 업데이트하고 푸시했을 때
git pull origin main
# ❌ 느리고 불확실한 방법
npm install # 기존 node_modules 유지, 부분 업데이트
# ✅ 빠르고 안전한 방법
npm ci # node_modules 삭제 후 lock 파일대로 정확히 재설치
결과:
- npm install: 2초 (기존 것 재사용) but 불완전할 수 있음
- npm ci: 16초 (전체 재설치) but 100% 정확
예제 4: CI/CD 파이프라인 (GitHub Actions)
시나리오: 자동화된 테스트 및 배포
# ❌ 잘못된 CI 설정
name: Test
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: npm install # ❌ 느리고, lock 파일 수정할 수 있음
- name: Run tests
run: npm test
# ✅ 올바른 CI 설정
name: Test
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# npm 캐시 활용 (더 빠름)
- name: Cache npm dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
- name: Install dependencies
run: npm ci # ✅ 빠르고, 항상 동일한 결과
- name: Run tests
run: npm test
성능 비교:
- npm install: ~130초
- npm ci: ~40초 (3배 빠름)
- npm ci + cache: ~10초 (13배 빠름)
예제 5: Docker 빌드 최적화
시나리오: 프로덕션 Docker 이미지 빌드
# ❌ 비효율적인 Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm install # 느리고, devDependencies도 설치됨
CMD ["node", "server.js"]
# ✅ 최적화된 Dockerfile
FROM node:18-alpine
WORKDIR /app
# 1. package files만 먼저 복사 (레이어 캐싱 활용)
COPY package*.json ./
# 2. npm ci로 프로덕션 의존성만 설치
RUN npm ci --production && \
npm cache clean --force
# 3. 소스 코드 복사 (package.json 변경 시에만 재설치)
COPY . .
CMD ["node", "server.js"]
장점:
- 레이어 캐싱: package.json 미변경 시 의존성 재설치 생략
- –production: devDependencies 제외 (이미지 크기 감소)
- npm ci: 빠르고 일관된 설치
- cache clean: 불필요한 캐시 제거
예제 6: package.json과 lock 파일 불일치 감지
시나리오: package.json을 수동으로 수정한 후
# package.json을 직접 편집
vim package.json
# "express": "^4.18.0" → "express": "^4.19.0"으로 변경
# ❌ npm install: 조용히 lock 파일 업데이트
npm install
# → package-lock.json이 자동으로 수정됨 (알아차리기 어려움)
# ✅ npm ci: 불일치를 즉시 감지
npm ci
# → 에러 발생!
# npm ERR! `npm ci` can only install packages when your package.json
# and package-lock.json are in sync. Please update your lock file with
# `npm install` before continuing.
핵심: npm ci는 불일치를 에러로 알려주므로, 의도하지 않은 버전 변경을 방지합니다.
동작 원리 깊이 이해하기
npm install의 내부 동작
1. package.json 읽기
↓
2. package-lock.json 확인
↓
3. 의존성 트리 해결 (semver 범위 고려)
↓
4. 설치 필요한 패키지 확인
↓
5. node_modules 업데이트 (기존 것 유지하며)
↓
6. package-lock.json 업데이트 (필요시)
시간이 오래 걸리는 이유: 의존성 트리 해결 과정이 복잡합니다.
npm ci의 내부 동작
1. package-lock.json 존재 확인 (없으면 즉시 에러)
↓
2. package.json과 일치 여부 확인 (불일치 시 즉시 에러)
↓
3. node_modules 삭제 (rm -rf node_modules)
↓
4. lock 파일의 정확한 버전 설치 (의존성 해결 생략)
↓
5. 완료 (파일 수정 없음)
빠른 이유: 의존성 해결 과정을 완전히 생략합니다!
성능 비교 (실제 측정)
시나리오 1: 캐시 없는 환경 (CI/CD)
| 프로젝트 크기 | npm install | npm ci | 차이 |
|---|---|---|---|
| 소형 (20개) | 45초 | 18초 | 2.5배 빠름 |
| 중형 (100개) | 130초 | 40초 | 3.25배 빠름 |
| 대형 (500개) | 380초 | 120초 | 3.17배 빠름 |
시나리오 2: 캐시 있는 환경 (로컬)
| 상황 | npm install | npm ci |
|---|---|---|
| node_modules 존재 | ~2초 (매우 빠름) | ~16초 (삭제 후 재설치) |
| node_modules 없음 | ~25초 | ~16초 (더 빠름) |
인사이트:
- CI/CD: npm ci가 압도적으로 빠름
- 로컬 (node_modules 존재): npm install이 더 빠름
- 로컬 (클린 설치): npm ci가 더 빠름
고급 활용 팁
1. npm ci 속도 더 빠르게 하기
# ✅ 오프라인 캐시 우선 사용
npm ci --prefer-offline
# ✅ 프로덕션 의존성만 설치 (devDependencies 제외)
npm ci --production
# ✅ 캐시 디렉토리 지정 (CI 최적화)
npm ci --cache .npm-cache
2. package-lock.json 충돌 해결
# 충돌 발생 시
git pull
# CONFLICT in package-lock.json
# ❌ 잘못된 해결
# 수동으로 편집하거나 한쪽 버전 선택
# ✅ 올바른 해결
git checkout --theirs package-lock.json # 또는 --ours
npm install # lock 파일 재생성
npm ci # 검증
git add package-lock.json
git commit
3. npm 버전에 따른 차이
# npm 8.6.0 이상에서는 더 엄격한 검증
npm --version
# 8.6.0+
npm ci
# → package.json과 package-lock.json 일치 여부를 더 엄격하게 검증
# → 불일치 시 즉시 에러
중요: npm@8.6.0 이전에는 lock 파일만 보고 설치했지만, 이후부터는 package.json과의 일관성도 검증합니다.
4. 플래그 일관성 유지
# ❌ 문제 발생 케이스
npm install --legacy-peer-deps
# → package-lock.json 생성
npm ci # 에러!
# → 플래그 없이 실행하면 lock 파일과 맞지 않음
# ✅ 올바른 사용
npm install --legacy-peer-deps
# → package-lock.json 생성
npm ci --legacy-peer-deps # 성공!
# → 동일한 플래그 사용
핵심: lock 파일을 생성할 때 사용한 플래그를 npm ci에서도 동일하게 사용해야 합니다.
흔한 실수 TOP 5
1. CI에서 npm install 사용
# ❌ CI에서 npm install (느리고 불안정)
jobs:
build:
steps:
- run: npm install
# ✅ CI에서 npm ci (빠르고 안정적)
jobs:
build:
steps:
- run: npm ci
2. npm ci로 개별 패키지 설치 시도
# ❌ npm ci는 개별 패키지 설치 불가
npm ci lodash
# npm ERR! `npm ci` does not support arguments
# ✅ npm install 사용
npm install lodash
3. package-lock.json을 .gitignore에 추가
# ❌ lock 파일을 무시하면 npm ci 사용 불가
echo "package-lock.json" >> .gitignore
# ✅ lock 파일은 반드시 커밋
git add package-lock.json
git commit -m "chore: add package-lock.json"
이유: npm ci는 lock 파일이 필수이며, 팀원 간 동일한 의존성을 보장하려면 lock 파일을 공유해야 합니다.
4. node_modules 삭제 시간 문제 무시
# ❌ 느린 삭제 과정 (특히 Windows)
npm ci
# → node_modules 삭제에 수십 초 소요
# ✅ CI에서는 클린 환경 사용
# GitHub Actions: 매번 새로운 컨테이너
# 또는 명시적으로 삭제 생략 (npm ci는 자동으로 삭제함)
5. 오래된 npm 버전 사용
# ❌ 오래된 npm (npm ci 미지원 또는 버그 있음)
npm --version
# 5.7.0 (npm ci는 5.7.1부터 지원)
# ✅ 최신 npm으로 업데이트
npm install -g npm@latest
npm --version
# 10.x.x
실전 의사결정 플로우차트
새 의존성 추가/제거가 필요한가?
├─ Yes → npm install <package>
└─ No → 계속
CI/CD 파이프라인인가?
├─ Yes → npm ci (항상!)
└─ No → 계속
로컬 개발 환경인가?
├─ Yes → node_modules가 이미 있는가?
│ ├─ Yes → npm install (빠름)
│ └─ No → npm ci (더 안전)
└─ No → 계속
프로덕션 배포인가?
├─ Yes → npm ci --production
└─ No → npm ci
실전 체크리스트
프로젝트에서 다음을 확인하세요:
로컬 개발
- package-lock.json을 버전 관리에 포함
- 새 프로젝트 클론 시
npm ci사용 - package 추가/제거 시
npm install <package>사용 - 의존성 업데이트 후
npm ci로 재설치
CI/CD
- npm ci 사용 (npm install 금지)
- npm 캐시 활용 (~/.npm)
- 프로덕션 빌드 시
npm ci --production사용 - npm 버전 최신으로 유지 (8.6.0+)
Docker
- package*.json을 먼저 COPY (레이어 캐싱)
- npm ci –production 사용
- npm cache clean –force 로 캐시 정리
팀 협업
- lock 파일 충돌 시 npm install로 재생성
- 플래그 일관성 유지 (–legacy-peer-deps 등)
- npm 버전 통일 (.nvmrc 또는 package.json engines)
마무리
npm install과 npm ci, 이제 차이를 명확히 아시겠나요?
핵심 요약:
- ✅ npm install: 로컬 개발, 패키지 추가/제거
- ✅ npm ci: CI/CD, 프로덕션, 클린 설치
- ✅ 속도: npm ci가 2-3배 빠름 (캐시 없을 때)
- ✅ 안정성: npm ci가 항상 동일한 결과 보장
- ✅ lock 파일: npm ci는 필수, npm install은 선택
베스트 프랙티스:
- CI/CD에서는 무조건 npm ci
- 로컬 클린 설치도 npm ci
- 개별 패키지 관리만 npm install
- package-lock.json은 반드시 커밋
다음에 이런 경우를 만나면:
- GitHub Actions 빌드 느림 → npm ci + 캐싱
- 팀원마다 다른 버전 설치 → npm ci로 통일
- Docker 빌드 최적화 → npm ci –production
- 새 패키지 추가 → npm install
여러분의 프로젝트에서는 어떤 명령어를 사용하시나요? CI/CD 파이프라인을 npm ci로 바꾸고 나서 어떤 변화가 있었는지 궁금합니다!
Happy installing! 📦⚡
댓글