2장에서 배울 내용
- package.json의 구조와 주요 필드 분석
- npm, node_modules의 개념을 명확히 이해
- 자바스크립트 프로젝트를 체계적으로 관리하는 데 필요한 기초 지식 쌓기
package.json 톺아보기
- 모든 JavaScript 프로젝트의 중심
- 제대로 이해하고 활용하면 프로젝트 관리와 최적화에서 큰 이점을 얻음
- 구조와 주요 필드를 분석하고, 프로젝트를 더 효율적으로 관리하는 방법 배우기
package.json이란?
package.json은 프로젝트에서 다음 세 가지 역할을 수행한다:
| 역할 | 설명 |
|---|---|
| 메타데이터 정의 | 프로젝트 이름, 버전, 설명 등 기본 정보 |
| 의존성 나열 | 패키지 실행에 필요한 외부 라이브러리 목록 |
| 스크립트 설정 | 빌드, 테스트 등 실행 가능한 명령어 정의 |
- JSON 형식으로 작성해야 함
- 주석 작성 등 일부 작업이 제한되는 것처럼 보이나 우회 방법이 존재
JSON은 원래 주석을 지원하지 않지만, "//" 키를 사용하거나 빌드 시 주석을 제거하는 전처리 도구를
활용하는 방식으로 우회할 수 있다.
주요 필드
name
프로젝트 이름을 선언하는 필드이다.
- 반드시 필요하지는 않음
- npm 레지스트리에 업로드하거나 내부적으로 다른 곳에서 참조할 목적이 없다면 없어도 무방
- 이미 업로드되어 있는 같은 이름을 중복해서 사용해도 상관없음
왜 name이 중요하지 않을 수 있을까?
웹 서비스를 운영하기 위해 만들어진 프로젝트는 빌드된 내용을 레지스트리에 업로드하는 것이 아니라, 빌드한 파일만 특정 서버에 업로드해서 사용하기 때문이다. 그러나 npm에 업로드해서 다른 개발자가 사용할 수 있게 만들 목적이라면 고유한 명칭을 지정해야 한다.
scope
@scope/package와 같은 형태로, 여러 패키지 사이에서 연관 있는 패키지를 묶고 싶을 때 사용한다.
{
"name": "@babel/core",
"name": "@types/react",
"name": "@my-company/utils"
}- npm에서 특별 취급: 사용자나 조직은 자신만의 스코프를 가질 수 있으며, 그 외의 사용자들은 해당 스코프를 사용하지 못함
- 이러한 특성 때문에 회사의 브랜드명을 알릴 수 있는 스코프를 선점해서 사용하기도 함
예를 들어, @google/, @facebook/, @vercel/ 등은 해당 회사만 사용할 수 있는 스코프이다. 자신의
회사나 팀이 있다면 스코프를 미리 등록해두는 것이 좋다.
version
- 웹에 서비스를 제공하는 용도라면 크게 상관없음
- npm 레지스트리에 업로드해야 한다면 반드시 신경 써야 하는 필드
- 항상 고유해야 함: 하나의 name에는 동일한 version이 존재할 수 없음
{
"name": "my-package",
"version": "1.0.0"
}1장에서 배운 유의적 버전(SemVer) 규칙에 따라 MAJOR.MINOR.PATCH 형태로 작성한다. 이미 배포된
버전과 동일한 번호로 다시 배포할 수 없으므로, 변경 사항이 있을 때마다 적절히 버전을 올려야 한다.
description
패키지에 대한 설명을 작성하는 필드이다.
# npm info로 패키지 설명 확인 가능
npm info <패키지명>keywords
패키지와 관련된 키워드를 입력하는 곳이다.
- 문자열 배열 형태로 작성
- 패키지를 설명할 수 있는 간단한 키워드
- 패키지가 의존성을 가지고 있는 프레임워크나 라이브러리명을 선언해두는 것이 일반적
{
"keywords": ["react", "hooks", "state-management", "typescript"]
}homepage
패키지의 홈페이지 URL을 기재하는 필드이다. GitHub 주소나 별도의 소개 사이트를 적는다.
bugs
패키지에 버그가 있을 경우 제보할 수 있는 주소나 이메일을 적는 필드이다.
{
"bugs": {
"url": "https://github.com/owner/project/issues",
"email": "bugs@example.com"
}
}보통 GitHub의 Issues 탭 주소를 사용한다.
라이선스 (license)
라이선스를 지정하는 필드로, 패키지에 어떤 제한이 있는지 알릴 수 있다.
자주 사용하는 라이선스는 OSI Approved Licenses 목록에서 찾을 수 있으며, 원하는 라이선스의 SPDX ID를 license 필드에 기재한다.
SPDX ID란? Software Package Data Exchange의 약자로, 각 라이선스를 고유하게 식별할 수 있는
식별자이다. 모든 소프트웨어에서 일관되게 사용하고 인지할 수 있도록 하는 데 목적이 있다. 예) MIT
라이선스의 SPDX ID = MIT
주요 라이선스 비교
| 라이선스 | 유래 | 특징 | 소스 공개 의무 |
|---|---|---|---|
| MIT | 매사추세츠 공과대학 | 제한이 매우 느슨, 안전하게 사용 가능 | 없음 |
| ISC | npm init 기본값 | MIT와 유사 + 저작권 선언 포함 의무 | 없음 |
| Apache 2.0 | 아파치 소프트웨어 재단 | 특허권 자동 부여가 특징 | 없음 |
| BSD | 버클리 캘리포니아 대학교 | 저작권자 이름 + 라이선스 내용 배포 필요 | 없음 |
npm 생태계에서는 제약 사항이 거의 없다는 것을 알리기 위해 MIT나 ISC가 가장 널리 사용된다.
UNLICENSED는 다른 사람이 사용하지 못하게 만든 것이기 때문에, 해당 패키지를 사용하는 사용자가 주의해야 한다.
기타 메타데이터 필드
author와 contributors
name, email, url 필드를 가진 person 객체를 사용할 수 있다는 공통점이 있다.
| 필드 | 대상 |
|---|---|
author | 한 명만 선언 가능 |
contributors | 여러 명 선언 가능 |
{
"author": {
"name": "홍길동",
"email": "hong@example.com",
"url": "https://hong.dev"
},
"contributors": [{ "name": "김개발" }, { "name": "이코딩" }]
}funding
패키지 개발에 직접적인 자금을 지원하는 방법에 대한 정보를 알려주는 필드이다.
- 객체, 배열 등 자유롭게 작성 가능
# 의존성에 있는 모든 funding 주소를 가져와서 보여줌
npm fund패키지 배포 관련 필드
files
패키지를 업로드하는 경우에 사용되는 매우 중요한 필드이다.
- npm 레지스트리에 업로드될 때 포함해야 할 파일 목록을 선언
- 잘 활용하면 꼭 필요한 파일만 선택적으로 배포 가능
- 불필요한 파일이나 디렉터리를 제외하여 패키지 크기를 줄이는 데 도움
.gitignore과 유사한 문법으로 선언
{
"files": ["dist/", "lib/", "README.md"]
}main
패키지의 **진입 파일(entry point)**을 의미하는 매우 중요한 필드이다.
{
"main": "dist/index.js"
}다른 프로젝트에서 require('my-package') 또는 import 'my-package'를 할 때, main 필드에 지정된
파일이 로드된다.
browser
브라우저와 같은 클라이언트 측에서 사용하고자 한다면 main 필드 대신 이 필드를 사용한다.
- webpack 같은 번들러가 이 필드를 참조함
- Node.js에서는 이 필드를 사용하지 않음
bin
패키지가 직접 바로 실행 가능한 파일을 가지고 있을 때, 실행 가능한 파일의 위치를 선언하는 필드이다.
{
"bin": {
"create-react-app": "./index.js"
}
}셔뱅(Shebang)이란?
Node.js로 실행될 파일을 bin 필드에 넣어뒀다면, 파일 상단에 반드시 #!/usr/bin/env node를 선언해야 한다. 이를 **셔뱅(Shebang)**이라고 하며, 스크립트의 최상단에 선언해서 이 파일이 어떤 인터프리터를 기반으로 실행되는지를 선언한다.
#!/usr/bin/env node
// 셔뱅을 선언하면 bin 필드에 node ./index.js처럼
// node를 지정하지 않아도 된다
console.log("Hello from CLI!");repository
실제 패키지의 코드가 있는 곳을 기재하는 필드이다.
{
"repository": {
"type": "git",
"url": "https://github.com/owner/project.git"
}
}// 축약 문법
{
"repository": "github:owner/project"
}모노레포의 경우 디렉터리 위치를 명시해주면 좋다:
{
"repository": {
"type": "git",
"url": "https://github.com/owner/monorepo.git",
"directory": "packages/my-package"
}
}스크립트 관련 필드
scripts
가장 자주 쓰이는 필드로, 기본적으로 제공하는 명령어뿐만 아니라 임의의 명령어를 선언해서 사용할 수 있다.
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint .",
"pretest": "echo '테스트 준비 중...'",
"test": "jest",
"posttest": "echo '테스트 완료!'"
}
}# 실행 방법
npm run-script <명령어>
npm run <명령어>pre, post 접두사 지원:
| 실행 명령 | 실행 순서 |
|---|---|
npm run test | pretest → test → posttest |
npm run build | prebuild → build → postbuild |
잘못 설정할 경우 무한 루프에 빠질 수도 있다. 이를 방지하려면 명령어 간의 순환 참조를 피해야 한다.
// ❌ 잘못된 예시 - 무한 루프!
{
"scripts": {
"build": "npm run prebuild",
"prebuild": "npm run build"
}
}config
scripts를 실행할 때 사용할 수 있는 다양한 설정 관련 값을 객체 형태로 모아둘 수 있다.
{
"config": {
"port": "3000"
}
}하지만 JSON 형태로 작성해야 해서 관리가 어렵고, 값에 접근하기 위해 접두사(npm_package_config_)가
필요하기 때문에, 개발 환경에서는 .env 파일을 만들고 환경 변수를 관리할 수 있는 패키지인
dotenv를 더 많이 사용하는 편이다.
의존성 관련 필드
dependencies
프로젝트가 실행되는 데 필요한 외부 패키지 및 라이브러리를 정의하는 필드이다. 프로젝트가 의존하는 패키지들과 해당 버전 범위가 명시된다.
{
"dependencies": {
"react": "^18.2.0",
"next": "^14.0.0",
"axios": "^1.6.0"
}
}dependencies 외에도 peerDependencies, devDependencies 등의 필드가 존재한다. 각 필드는
의존성의 용도와 설치 시점에 따라 구분된다.
overrides
패키지 자신이 참조하고 있는 의존성의 의존성(간접 의존성) 버전을 수정하고 싶을 때 유용하다.
{
"overrides": {
"vulnerable-package": "2.0.1"
}
}주로 긴급한 보안 위협이 발생해서 빠르게 대응하고자 할 때, 해당 패키지의 버전업을 기다릴 수 없는 경우에 사용한다.
overrides 사용 시 반드시 주의를 요한다. 의존성 체인을 강제로 변경하는 것이므로, 사용에 따른 책임은 프로젝트 개발자에게 있다. 가능하면 해당 패키지의 공식 업데이트를 기다리는 것이 안전하다.
engines
패키지가 실행 가능한 Node.js 버전을 명시할 수 있다.
{
"engines": {
"node": ">=18.0.0",
"npm": ">=9.0.0"
}
}팀 프로젝트에서 engines 필드를 설정해두면, 다른 팀원이 호환되지 않는 Node.js 버전으로 프로젝트를 실행하려 할 때 경고 메시지를 표시할 수 있어 유용하다.
기타 설정 필드
os
패키지가 실행 가능한 운영체제를 선언하고 싶을 때 사용하는 필드이다.
{
"os": ["darwin", "linux"],
"os": ["!win32"]
}아주 특별한 이유가 있는 것이 아니라면 거의 사용되지 않는다.
cpu
특정 CPU 아키텍처를 요구할 수 있는 필드이다.
{
"cpu": ["x64", "arm64"]
}private
true로 설정 시 npm은 해당 패키지를 절대로 npm 레지스트리에 업로드하지 않는다.
{
"private": true
}우발적 배포를 막는 최선의 보호 장치이다. 사내 프로젝트나 웹 서비스 프로젝트에서는 반드시
true로 설정해두는 것이 좋다.
publishConfig
패키지를 배포할 때 필요한 설정 값을 선언할 때 사용한다.
{
"publishConfig": {
"registry": "https://npm.pkg.github.com",
"access": "public"
}
}workspaces
npm@7부터 도입된 워크스페이스 기능을 지원하기 위한 필드이다. 워크스페이스란 기본적으로 하나의 최상위 패키지 위에서 하위 여러 패키지를 관리하기 위한 방식을 의미한다.
{
"workspaces": ["packages/*"]
}워크스페이스를 사용하면 최상위에 하나의 node_modules와 package-lock.json이 생기는 대신, 하위 패키지들은 최상위에 있는 node_modules를 보고 자신이 필요한 패키지들을 참조하거나 패키지 간에 서로 참조가 가능해진다.
워크스페이스 관리를 위한 도구로는 이전부터 Lerna, Nx, Turborepo 등이 존재한다.
packageManager
npm에서는 공식적으로 사용되지 않지만, Node.js에서 사용하는 필드이다. 예상되는 패키지 관리자를 지정할 수 있다.
{
"packageManager": "pnpm@9.1.0"
}Corepack과 함께 유용하게 쓸 수 있다. Yarn Berry의 경우 Corepack을 통한 설치를 공식적으로 권장한다.
2026년 2월 기준 packageManager 필드 상태
2026년 2월 기준으로 packageManager 필드를 실질적으로 활용하는 Corepack은 여전히 Node.js에서 실험적(experimental) 상태이다. 더 중요한 변화로, Node.js TSC(Technical Steering Committee)가 Node.js 25부터 Corepack을 Node.js에 포함하지 않기로 공식 결정했다. Node.js 24까지는 기존처럼 실험적 기능으로 포함되지만, 이후에는 별도의 npm 패키지로 설치해야 한다.
제거 결정의 주요 이유:
- 광범위한 채택 부족 (많은 개발자가 Corepack 없이 패키지 관리자를 직접 설치)
- Node.js 바이너리에 불필요한 도구 번들링에 대한 우려
- 패키지 관리자가 Node.js와 독립적으로 발전할 수 있도록 하기 위함
다만, Corepack 자체는 독립 npm 패키지로 계속 유지되며 Yarn과 pnpm에서는 여전히 권장하는 설치 방식이다.
type
자바스크립트 생태계에서 지원하는 모듈 시스템인 CommonJS와 ESModule 중 Node.js가 어떤 모듈 형식을 사용할지 알리는 필드이다.
{
"type": "module"
}| 값 | 설명 |
|---|---|
"module" | ESModule 방식으로 해석 |
"commonjs" | CommonJS 방식으로 해석 (기본값) |
선언하지 않으면 commonjs가 기본값이다.
exports
main의 대안으로, 해당 패키지를 설치해서 사용하는 사용자에게 패키지의 진입점을 나타낼 수 있는 필드이다. 응용하면 조건부 내보내기가 가능하다.
{
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./utils": "./dist/utils.js"
}
}imports
해당 패키지 내부에서만 쓸 수 있는 구문으로, tsconfig의 경로 별칭을 지정할 수 있는 compilerOptions.paths와 동일하게 특정 불러오기에 대해 별칭을 지정할 수 있는 기능이다.
{
"imports": {
"#utils": "./src/utils/index.js",
"#components/*": "./src/components/*.js"
}
}기타
위에서 언급한 예약어 외에 다른 필드에 대한 제한이 없어서 임의로 지정해서 사용 가능하다.
package.json 생성하기
npm init
package.json을 생성하는 가장 편리한 명령어이다.
# 대화형으로 생성
npm init
# 문답 없이 기본값으로 생성
npm init --yespackage.json에 주석 추가하기
JSON 파일에는 주석 삽입이 어렵다. "//" 키를 사용하여 넣을 수 있으며, 이는 npm 팀에서 공식적으로 권장하는 방식이다.
{
"//": "이것은 주석입니다",
"name": "my-package"
}scripts 안에는 "//" 키를 사용할 수 없다. 다른 방법으로는 사용되지 않는 예약어를
활용하거나, 접두사 등의 규칙을 정해서 작성하는 방법이 있다.
npm config와 .npmrc 살펴보기
npm config 주요 설정
| 설정 | 설명 |
|---|---|
_auth | 기본값 null. npm 레지스트리에 인증할 때 사용되는 문자열 값 |
registry | npm 패키지가 업로드되는 데이터베이스 주소 |
engine-strict | engines 필드를 엄격하게 적용할지 여부를 결정하는 값 |
access | 한번 public으로 설정되면 이후에 restricted로 변경 불가 |
legacy-peer-deps | npm@7부터 peerDependencies를 엄격하게 검사하게 되어, 맞지 않으면 설치가 중단됨. 이 옵션으로 이전 동작으로 되돌릴 수 있음 |
주요 레지스트리 목록
| 레지스트리 | 설명 |
|---|---|
| npm 공식 레지스트리 | https://registry.npmjs.org/ |
| GitHub 패키지 레지스트리 | https://npm.pkg.github.com/ |
| Yarn 레지스트리 | npm 공식 레지스트리의 CNAME |
| cnpm 레지스트리 | 중국 내부에서 운영하는 미러 레지스트리 |
| 기타 사설 레지스트리 | 회사/조직 내부에서 운영하는 레지스트리 |
CNAME이란? Canonical Name의 약자로, DNS 레코드 유형 중 하나이다. 한 도메인 이름을 다른 도메인 이름으로 매핑하는 데 사용된다. Yarn 레지스트리는 실제로 npm 공식 레지스트리를 가리키는 CNAME이다.
.npmrc 파일 다루기
앞서 언급한 config 관련 내용을 기재해 둘 수 있으며, 크게 네 곳에 위치할 수 있다:
| 위치 | 우선순위 |
|---|---|
프로젝트 최상단 (.npmrc) | 1순위 (가장 높음) |
사용자 홈 디렉터리 (~/.npmrc) | 2순위 |
| 글로벌 구성 파일 | 3순위 |
| npm 내장 구성 파일 | 4순위 (가장 낮음) |
- 파일 내 설정은 모두
키 = 값형태로 지정 - 프로젝트 최상위 경로에 위치한
.npmrc파일이 가장 높은 우선순위를 가진다
# 현재 적용된 설정 확인
npm config listdependencies란 무엇일까?
프로젝트를 진행할 때 가장 자주 사용하며, 명확한 이해가 중요하다.
| 필드 | 용도 |
|---|---|
dependencies | 런타임에서 필요한 패키지를 관리 |
devDependencies | 개발 중에만 필요한 패키지를 관리 |
peerDependencies | 플러그인/라이브러리가 호환성을 유지해야 하는 경우 사용 |
npm 버전과 버전에 사용되는 특수 기호
| 기호 | 이름 | 의미 | 예시 |
|---|---|---|---|
| (없음) | 고정 버전 | 정확히 일치하는 버전만 설치. npm update해도 영향 없음 | "1.0.0" |
^ | 캐럿 | 마이너 + 패치 업데이트까지 용인. 명시된 버전을 최소로 기준 삼아 최신 버전 설치 | "^1.2.3" → 1.x.x |
~ | 틸드 | 패치 버전 변경까지만 용인. 캐럿보다 더 엄격한 방식 | "~1.2.3" → 1.2.x |
* | 애스터리스크 | 아무 버전이나 상관없음. 실무에서 거의 사용되지 않음 | "*" |
~(틸드)는 신규 API가 추가될 것으로 예상되는 마이너 버전의 변경까지 막을 수 있어, 보수적으로
버그 수정에 대해서만 대응하고 싶은 경우에 사용한다.
패키지명 대신 git, file:, http:// 등을 사용하면 패키지명을 무엇으로 하든 설치할 수 있고, 코드 내에서 변경된 이름으로 사용하는 것도 가능하다.
올바른 버전을 선언했는지 확인하고 싶다면? https://semver.npmjs.com/ 에서 테스트할 수 있다.
dependencies 필드
npm install을 실행할 때 이 항목에 정의된 패키지들이 자동으로 설치된다. 또한 배포된 패키지가 다른 프로젝트에 설치될 때도 dependencies에 명시된 패키지는 항상 함께 설치된다.
A가 B에 의존 → C가 A에 의존
A 패키지에서 npm install 실행 시 → B가 설치됨
C 패키지에서 npm install 실행 시 → A와 B가 모두 설치됨
이처럼 의존성은 연쇄적으로 전파되므로, 신중하게 선언해야 한다. 불필요한 패키지가 사용자에게까지 설치될 수 있기 때문이다.
devDependencies
개발할 때만 필요한 패키지를 의미한다. 해당 패키지를 사용하는 사용자가 아닌, 해당 패키지를 개발하는 개발자가 필요로 하는 패키지를 선언한다.
{
"devDependencies": {
"typescript": "^5.0.0",
"eslint": "^8.0.0",
"jest": "^29.0.0"
}
}peerDependencies
npm 패키지를 만들거나 배포할 때 중요한 필드이다.
일부 특수한 경우에는 특정 패키지를 직접 require하지 않으면서도 호스트 도구 또는 라이브러리와 패키지의 호환성을 표현하고자 하는 경우가 있다. 이는 보통 플러그인이라고 불리며, 호스트 문서에서 기대되고 명시된 특정 인터페이스를 노출하는 모듈일 수 있다.
요즘 의미로 peerDependencies:
- 호환성 선언 용도
- 사용자에게 특정 패키지 설치에 주의를 주는 용도
즉, 특정 호스트 패키지를 기반으로 작성된 패키지를 만들고 싶을 때 반드시 사용해야 하는 필드이다.
예시: React 커스텀 훅 패키지
const useSum = (a, b) => {
return a + b;
};
export default useSum;{
"name": "use-sum",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
}peerDependencies를 쉽게 이해하기
위 use-sum 패키지는 React의 커스텀 훅 네이밍 컨벤션(use 접두사)을 따르는 패키지이다. 이 패키지는 React 프로젝트 안에서만 의미가 있으므로, React가 이미 설치되어 있어야 한다.
하지만 use-sum이 직접 React를 dependencies에 넣으면 문제가 생긴다. 사용자의 프로젝트에 이미 React가 설치되어 있는데, use-sum이 또 다른 버전의 React를 설치하면 중복 설치와 버전 충돌이 발생할 수 있다.
이때 peerDependencies를 사용하면:
- "나는 React 16.8 이상이 필요해, 하지만 내가 직접 설치하지는 않을게"
- "대신 네 프로젝트에 이미 설치되어 있는 React를 사용할게"
라는 의미가 된다. 비유하자면, **핸드폰 케이스(플러그인)**가 핸드폰(호스트 패키지)을 포함해서 판매하지 않는 것과 같다. 케이스는 "이 케이스는 iPhone 14, 15, 16에 호환됩니다"라고 알려줄 뿐, 핸드폰은 사용자가 이미 가지고 있어야 한다.
peerDependenciesMeta
peerDependencies에 지정된 패키지가 설치되지 않은 경우 에러를 반환하지만, peerDependenciesMeta는 선택적인 호스트 패키지를 선언할 때 유용하다. 반드시 peerDependencies와 함께 사용해야 한다.
{
"peerDependencies": {
"react": "^18.0.0",
"react-native": "^0.72.0"
},
"peerDependenciesMeta": {
"react-native": {
"optional": true
}
}
}위 예시에서 react는 필수이지만, react-native는 선택적이어서 설치되지 않아도 에러가 발생하지 않는다.
npm의 주요 명령어
npm run
가장 자주 쓰이는 명령어이다. scripts 뒤에 명령어를 찾아서 실행한다. 인수를 전달하고 싶을 때는 --를 사용한다.
npm run build
npm run test -- --coverage재미있는 점: eslint 예제
{
"scripts": {
"lint": "eslint ."
}
}"eslint ." 같은 간단한 명령어를 왜 스크립트에 추가할까? bash에서 직접 eslint .를 사용하면 작동하지 않는다. npm run이 명령어 실행 시 보이지 않는 작업을 처리하기 때문이다.
npm run은 실행 시 node_modules/.bin을 PATH에 자동으로 추가한다. 따라서 npm run lint는
사실상 node_modules/.bin/eslint .와 동일하다. npm run script에서 script가 실행되는 위치와 실제
bash에서 실행되는 위치가 다를 수 있기 때문에, 실행 결과에도 차이가 발생할 수 있다는 점을 기억하자.
npm install과 npm ci
두 명령어 모두 의존성을 설치한다는 공통점이 있지만, 쓰임새는 사뭇 다르다.
| 항목 | npm install | npm ci |
|---|---|---|
package-lock.json 필요 여부 | 없어도 실행 가능 | 반드시 필요 (없으면 실행 불가) |
| 설치 기준 | package.json 기준으로 설치하고, package-lock.json을 새롭게 생성 (이미 있으면 그 내용에 따라 설치) | package-lock.json의 내용을 그대로 설치 |
package-lock.json 수정 여부 | 수정할 수 있음 | 절대 수정하지 않음 |
| 주 사용처 | 개발 환경 | CI/CD 환경 (재현 가능한 빌드) |
npm update
의존성을 업데이트하는 명령어이다. package.json에 명시된 버전 범위를 기준으로 업데이트를 진행한다.
# 모든 의존성 업데이트
npm update
# package.json에도 업데이트 반영
npm update --savenpm dedupe
현재 패키지 트리를 기반으로 의존성을 단순화(평탄화) 하는 명령어이다. 패키지 트리 간에 해결할 수 있는 중복 의존성이 있다면 사용한다.
# 의존성 평탄화
npm dedupe
# 미리 결과 확인 (실제 변경 없음)
npm dedupe --dry-run
# npm install 실행할 때 함께 실행
npm install --prefer-dedupe
# 항상 함께 실행되도록 설정
npm config set prefer-dedupe truenpm dedupe는 안전한가?
npm dedupe는 일반적으로 안전하다. 패키지 버전을 변경하는 것이 아니라, 이미 설치된 패키지들의 트리 구조만 재배치하여 중복을 제거하는 작업이기 때문이다. 유의적 버전(SemVer) 규칙 안에서만 동작하므로 호환성 문제가 발생할 가능성이 낮다.
다만 프로덕션 환경에서 바로 적용하기보다는:
npm dedupe --dry-run으로 변경 사항을 먼저 확인- 테스트를 충분히 수행한 후 적용
하는 것을 권장한다.
Yarn에서의 dedupe:
- Yarn Classic (v1): 내장 dedupe 기능이 없어
yarn-deduplicate라는 서드파티 패키지를 사용해야 한다. - Yarn Berry (v2+):
yarn dedupe명령어가 내장되어 있다.yarn dedupe --check로 미리 확인도 가능하다. - pnpm: 심볼릭 링크 기반 구조 특성상 중복 설치가 원천적으로 적어 별도의 dedupe 명령어가 필요하지 않다.
npm ls
패키지 내에 설치되어 있는 모든 의존성을 보여준다.
# 전체 의존성 트리 확인
npm ls
# 특정 패키지의 의존성 확인
npm ls <패키지명>
# 깊이 제한 (최상위만 보기)
npm ls --depth=0npm explain
뒤에 반드시 패키지명이 와야 한다. 대상 의존성이 정확히 왜 설치됐는지에 대한 정보까지 나타낸다.
npm explain <패키지명>npm audit
npm에서 제공하는 보안 취약점을 검사하는 명령어이다.
# 보안 취약점 검사
npm audit
# 취약점 자동 해결 (유의적 버전 범위 내에서)
npm audit fix
# 유의적 버전을 무시하고 강제 해결 (주의 필요!)
npm audit fix --force취약점 발견 시, 본격적으로 해결하기에 앞서 npm explain으로 왜 설치됐는지 먼저 확인하는 것이
좋다. npm audit fix는 유의적 버전을 준수하는 선에서 해결이 가능한 경우에만 동작하며, --force
옵션은 버전 호환성 문제가 발생할 수 있으므로 신중하게 사용해야 한다.
npm publish
현재 패키지를 레지스트리에 업로드하는 명령어이다. 모든 파일이 업로드되는 것이 아니라, 다음 규칙에 따른다:
| 규칙 | 설명 |
|---|---|
| 자동 포함 | package.json, README.md, LICENSE |
| 자동 제외 | node_modules, .DS_Store, .svn |
.npmignore 존재 시 | 해당 파일에 명시된 파일/폴더 제외. .gitignore보다 우선 |
files 필드 존재 시 | 해당 파일만 업로드 |
npm deprecate
업로드되어 있는 특정 패키지에 대해 사용자에게 경고 메시지를 보여주는 명령어이다. 지원 중단되었다는 것을 의미한다.
# 지원 중단 처리
npm deprecate <패키지명>@<버전> "이 버전은 더 이상 지원되지 않습니다. v2로 업그레이드해주세요."
# 지원 중단 처리 되돌리기 (빈 문자열 지정)
npm deprecate <패키지명>@<버전> ""npm outdated
현재 설치된 패키지 중 현재 시간 기준 최신 버전이 아닌, 업데이트가 가능한 패키지를 볼 수 있는 명령어이다.
npm outdated무조건 업데이트한다고 좋은 것은 아니다. 실제로 설치해서 사용하기 전에 반드시 변경 사항과 호환성을 확인하자.
npm view
특정 패키지의 상세 정보를 확인하는 명령어이다.
npm view <패키지명>
npm view <패키지명> versions # 모든 버전 확인더 다양한 명령어
npm 공식 문서에서 전체 명령어 목록을 확인할 수 있다: https://docs.npmjs.com/cli/v10/commands/npm
npm install을 실행하면 벌어지는 일
"오픈소스를 사용하다 보면 종종 자신의 코드 밖에서 일어나는 일을 이해해야만 고칠 수 있는 문제들도 생기는데, 이 과정을 이해한다면 node_modules에서 발생하는 문제의 실마리도 찾을 수 있을 것이다"
@npmcli/arborist - 의존성 트리 분석의 핵심
node_modules와 package.json의 트리를 관리하기 위한 CLI 도구이다. Node.js 런타임에서 분석을 수행한다.
Arborist 클래스의 대표적인 메서드:
loadActual
node_modules 내부의 실제 트리를 확인할 수 있는 메서드이다. 파일 시스템을 직접 스캔해서 node_modules 디렉터리 내 모든 패키지를 검색하고, 이 과정에서 패키지 간의 의존성 관계를 파악해 전체 의존성 트리를 구성한다.
트리 분석은 npm install 과정에서 중요한 역할을 한다. package.json에 선언된 의존성과 실제로 node_modules에 설치된 패키지가 일치하는지 확인하기 위해서이다.
ArboristNode
의존성 트리 내 각 노드를 나타내는 객체이다. ArboristNode 클래스의 인스턴스이며, 노드와 관련된 중요한 정보를 담고 있다.
| 속성 | 설명 |
|---|---|
name | 패키지 이름 |
version | 패키지 버전 |
location | 트리 내 위치 |
path | 파일 시스템 경로 |
resolved | 실제 다운로드 URL |
edgesIn | 이 노드를 의존하는 Edge 목록 |
edgesOut | 이 노드가 의존하는 Edge 목록 |
type | 의존성 유형 |
노드의 의존성은 Edge라는 객체로 표현된다.
loadVirtual
가상의 트리를 만드는 메서드이다. 파일 시스템을 직접 스캔하는 것이 아닌, package-lock.json이나 npm-shrinkwrap.json을 기반으로 의존성 트리를 생성한다. 두 파일의 정보를 바탕으로 이상적인 트리를 메모리에 가상으로 생성하는 과정이다.
대표적으로 npm ci에서 사용된다. npm ci는 package-lock.json을 기반으로 정확한 트리를 재현해야
하므로, 가상 트리를 먼저 만들어 기준으로 삼는다.
buildIdealTree
라이브러리의 핵심 메서드이다. package.json과 package-lock.json을 바탕으로 가장 이상적인 트리가 만들어진다.
이상적인 트리란 package.json에 선언된 의존성 버전을 충족하면서, 중복 설치를 최소화하고 버전 충돌을 최소화한 구조를 의미한다.
충돌이란? 같은 이름의 패키지가 동일한 위치에 이미 존재하거나, 부모 노드가 해당 패키지의 peerDependencies를 요구하는데 그 조건을 충족하지 못하는 상황
buildIdealTree의 과정:
initTree-package.json기반으로 노드 생성 →loadVirtual을 통해 가상 트리 로드inflateAncientLockfile- 오래된 락파일이 있는지 확인applyUserRequests- 사용자의 요청을 작업에 반영buildDeps- 락파일 최신화fixDepFlags- 플래그 설정pruneFailedOptional- 실패 작업이 존재한다면 이 과정에서 에러 발생checkEngineAndPlatform- 최적의 트리에서 버전 검사 및 현재 환경 호환성 확인 후 에러 발생 또는 경고 메시지 출력
reify
이상적인 트리를 실제로 구현하는 역할이다. "구현한다"는 뜻은 node_modules에 설치하고 package-lock.json에 반영하는 과정을 의미한다.
reify의 과정:
- 실행되는 위치 확인,
node_modules디렉터리 존재 확인 (없다면 생성) - actual 트리와 ideal 트리를 각각 생성
- 현재 상태를 나타내는 actual과 이상적인 상태를 나타내는 ideal을 비교
- 두 트리 간의 차이를 바탕으로 변경 사항 확인, 필요 시 패키지를
node_modules에 설치하거나 제거 - 모든 변경이 완료되면 이상적인 트리의 내용을 현재 트리에 복사해서 동기화 (무결성 유지)
- 의존성에 대한 보안 취약점 검사, 발견 시 정보 제공
reify를 실행하는 것만으로도 거의 npm install을 수행하는 것과 유사한 효과를 얻는다. npm install과 npm ci 모두 내부적으로 reify를 사용한다.
audit
보안 취약점을 살펴보기 위해 사용한다. AuditReport 클래스에서 수행된다.
- 취약점을 분석해야 하는 패키지 목록을 가져온다
- 패키지 목록을 가져온 다음 취약점 분석을 위한 정보를 가져온다
- 불러온 정보를 바탕으로 취약점을 분석하고 보고한다
pacote - 패키지 설치를 위한 패키지
실제 패키지를 npm에서 가져오는 역할을 한다. npm 패키지의 다운로드와 관리를 수행하는 라이브러리이다.
manifest
패키지의 manifest 정보를 가져오는 메서드이다.
manifest란?
manifest는 npm 레지스트리에 저장된 패키지의 메타데이터 객체이다. 쉽게 말하면 package.json의 가공된 버전이라고 할 수 있다. 다음과 같은 정보를 포함한다:
name,version- 패키지 기본 정보dependencies,devDependencies- 의존성 목록dist- 실제 tarball 다운로드 URL과 무결성 해시(integrity hash)engines,os,cpu- 호환성 정보
npm install 시 npm은 먼저 manifest를 가져와서 어떤 버전을 설치할지, 어디서 다운로드할지를 결정한다. 실제 패키지 코드를 다운로드하기 전에 이 정보를 먼저 확인하는 것이다.
tarball
tarball 데이터를 메모리에 불러오는 작업을 수행한다. 이 데이터는 .tgz 확장자의 Buffer 형태로 반환된다.
extract
tarball이 .tgz 파일을 메모리에 불러오는 작업이라면, extract는 tarball을 불러오는 것을 넘어서 파일을 압축 해제해서 파일 시스템에 저장한다.
node_modules 구조 살펴보기
평탄화된 node_modules
평탄화되지 않은 node_modules는 구조가 직관적이지만 매우 비효율적이다:
- 동일한 패키지의 중복 설치 문제 - 같은 패키지가 여러 곳에 설치됨
- 경로 길이 문제 - 깊어질수록 경로가 길어짐
- Windows: 260자 제한
- macOS: 1024자 제한
- Linux: 4096자 제한
평탄화 작업을 통해 이러한 문제를 해결하지만, 부작용으로 실제로는 의존성을 선언하지 않은 라이브러리들이 실행 가능해진다. 이를 유령 의존성(Phantom Dependency) 이라고 한다.
npm이 중복 설치를 피하는 방법
평탄화 작업은 어디까지나 유의적 버전의 문법 규칙 안에서 해결 가능할 때만 이루어진다. 버전이 호환되지 않는 경우에는 여전히 중첩된 node_modules 구조가 사용된다.
node_modules는 무엇일까?
역할
| 역할 | 설명 |
|---|---|
| 의존성 관리 | 프로젝트가 필요로 하는 모든 외부 패키지를 저장 |
| 경로 해결 | Node.js의 모듈 해석 알고리즘이 패키지를 찾는 기준 |
| 네임스페이스 관리 | 스코프 패키지(@scope/pkg) 등의 구조적 분리 |
구조
.bin 디렉터리
package.json에서 정의된 bin 필드를 참조한다. 이 bin 필드는 패키지의 실행 가능한 스크립트 혹은 바이너리 파일을 정의하는 데 사용된다.
해당 폴더에 심볼릭 링크를 저장하며, 심볼릭 링크 덕분에 터미널의 프로젝트 경로상에서 패키지의 명령어를 직접 실행할 수 있다. 마치 전역에서 설치된 패키지와 동일한 방식으로 동작한다.
서브패키지 node_modules 폴더
해당 구조 덕분에 서로 다른 버전의 의존성을 사용하는 여러 패키지가 충돌 없이 필요한 모듈을 참조할 수 있으며, 안정성과 모듈화가 효율적으로 관리된다.
.cache 디렉터리
성능 향상을 위해 캐시 데이터를 저장하는 장소로 사용된다. 공식적으로 지정된 폴더는 아니지만 암묵적으로 사용되며, 사실상 표준 관행이다.
대표적인 예로 webpack과 Babel이 컴파일된 모듈과 기타 빌드 아티팩트를 해당 폴더에 저장한다.
심볼릭 링크
파일 시스템 내에서 특정 파일이나 디렉터리에 대한 참조를 다른 위치에 생성하는 기능이다. Node.js뿐만 아니라 대부분의 유닉스 계열 운영체제와 일부 다른 운영체제에서도 지원되는 일반적인 개념이다.
심볼릭 링크는 실제 파일의 복사본이 아닌, 원본 경로를 참조해서 원본 파일에 접근하는 일종의 링크 역할을 한다. 심볼릭 링크를 통해 원본 파일의 변경 사항을 링크가 참조하는 모든 위치에서 즉시 반영할 수 있다.
직접 활용하기 - npm link
패키지 구현 후 해당 package.json 파일이 위치한 경로에서 npm link 명령어를 실행하면, 이 패키지의 심볼릭 링크를 전역에 생성할 수 있다.
npm link는 개발 중인 패키지를 다른 프로젝트에서 테스트하거나 쉽게 사용할 수 있도록 돕는 명령어로, 심볼릭 링크를 통해 개발 중인 패키지를 전역으로 설치하거나 다른 프로젝트의 node_modules 폴더에 연결할 수 있다.
# 1. 개발 중인 패키지 디렉터리에서 심볼릭 링크 생성
npm link
# 전역 모듈 경로 확인
npm root -g
# 2. 다른 프로젝트에서 심볼릭 링크를 통해 패키지 사용
npm link <패키지명>
# 심볼릭 링크 해제
npm unlink <패키지명>
npm rm -g <패키지명>node_modules에서 심볼릭 링크의 활용
node_modules/.bin 경로에는 외부 패키지의 bin 필드에 정의된 실행 파일들이 심볼릭 링크 형태로 추가된다.
node_modules/
├── .bin/
│ └── eslint --> ../eslint/bin/eslint.js
└── eslint/
└── bin/
└── eslint.js
이처럼 node_modules/.bin 폴더는 프로젝트 내에 설치된 CLI 도구들을 효율적으로 관리하고 쉽게 실행할 수 있도록 돕는 중요한 역할을 한다.
eslint 명령어를 직접 터미널에서 쓸 수 없는 이유는, 로컬 프로젝트에 설치되고 실행 파일이 전역
경로에 추가되지 않기 때문에 eslint가 설치된 node_modules/.bin 경로를 알 수 없기 때문이다.
package.json의 scripts가 자동으로 로컬 node_modules/.bin 경로를 PATH 환경변수에
추가하므로 사용이 가능해진다.
워크스페이스에서의 심볼릭 링크
워크스페이스 기능에서도 심볼릭 링크가 활용된다. 워크스페이스란 여러 패키지를 하나의 프로젝트 내에서 함께 관리할 수 있게 해주는 기능이다. 워크스페이스를 사용하면 각 패키지가 node_modules 폴더 내에서 심볼릭 링크로 연결되어, 의존성을 설치할 때 별도의 패키지 관리 없이도 각 패키지 간의 참조가 자동으로 처리된다.
pnpm에서의 심볼릭 링크
패키지 관리자 pnpm에서도 특별한 방식으로 활용한다. 디스크 공간 절약과 설치 속도 향상을 위해 심볼릭 링크를 적극적으로 활용한다. (3.3절에서 상세히 다룰 예정)
bin 필드와 npx
bin 필드를 통해 실행 파일을 지정하고, 사용자가 명령어를 직접 호출할 수 있게 설정한다. npx는 특정 패키지를 로컬이나 전역에 설치하지 않고도 명령어를 한 번 실행할 수 있게 해주는 유용한 도구이다.
CLI 패키지
명령줄 인터페이스(Command Line Interface)를 통해 사용되는 소프트웨어 패키지이다.
장점:
- 반복 작업 자동화
- 프로젝트 구조화
- 사용자 친화적인 인터페이스
bin 필드 설정하기
package.json에서 정의되며, 패키지에서 실행 가능한 스크립트 또는 바이너리 파일을 지정하는 역할을 한다. CLI 패키지를 개발할 때 전역 또는 로컬로 실행 가능한 명령어를 설정할 수 있다.
기본적으로 bin 필드에 실행할 스크립트 파일의 상대 경로를 지정하면, 패키지 이름 자체가 실행 가능한 명령어로 사용된다. 객체로 정의해서 여러 명령어를 설정할 수도 있다.
// 단일 명령어
{
"name": "my-cli",
"bin": "./cli.js"
}
// 여러 명령어
{
"bin": {
"my-cli": "./cli.js",
"my-tool": "./tool.js"
}
}bin 필드 사용 조건:
- 실행 가능 권한 설정:
chmod +x 파일 - 셔뱅 라인 추가:
#!/usr/bin/env node
패키지가 다른 패키지의 의존성으로 설치될 때, bin 필드에 정의된 파일은 다음 두 가지로 사용할 수 있도록 링크된다:
npx혹은npm exec를 통해 직접 접근npm run-script로 호출
npx
개발자가 패키지를 직접 설치하지 않고도 npm 레지스트리에서 패키지를 실행할 수 있는 기능을 제공한다.
주요 용도:
| 용도 | 예시 |
|---|---|
| 로컬 패키지 실행 | npx eslint . |
| 전역 설치 없이 실행 | npx create-react-app my-app |
| 특정 버전의 패키지 실행 | npx node@18 index.js |
| 스크립트 실행 | npx ts-node script.ts |
npx는 어떻게 패키지를 찾아서 실행할까?
- 입력 명령어 파싱
- 로컬 패키지 탐색 (
node_modules/.bin) - 캐시 확인
- 레지스트리에서 패키지 검색
- 패키지 설치 (전역으로 설치되지 않고 일시적으로 저장)
- 패키지 실행
- 패키지 정리 (임시 파일 제거)
npx와 npm exec의 차이점
| 항목 | npx | npm exec |
|---|---|---|
| 도입 시기 | npm@5.2.0 | npm@7 |
| 인수 전달 | 직접 전달 | -- 뒤에 전달 |
| 패키지 미설치 시 | 자동 설치 후 실행 | 확인 프롬프트 표시 |
| 사용 편의성 | 간결한 문법 | 더 명시적인 문법 |