npm 기본기 다지기 - package.json과 npm 명령어 완전 정복
들어가며
새로운 회사에 입사하게 되면서 기본기를 다시 탄탄히 다져야겠다는 생각이 들었다. 그동안 개발을 하면서 당연하게 사용해왔던 도구들과 개념들을 다시 한번 차근차근 살펴보고 싶었고, 마침 좋은 책을 발견해서 읽고 있는데 바로 npm deep dive라는 책이다.

지난번 1장에서 Node.js의 기본 개념들을 정리했고, 이번에는 2장 내용을 정리해보려고 한다. 이번에 취업하게 되면서 팀에 배치를 받고 팀의 프로젝트 코드를 살펴보면서 이 책을 읽기 시작한 선택이 정말 잘한 일이었다는 생각이 더욱 강해지고 있다. 익숙하다고 생각했던 것들도 실제로는 모르는 부분이 많았다는 걸 깨닫고 있기 때문이다. 동기의 팁으로 프로젝트 package.json을 보면서 내가 사용해본 것과 모르는 것들을 나누고 찾아보고 있는데 이 책을 잘 선택했다는 생각을 했던 것 같다.
평소에 우리가 프로젝트를 만들고 개발에 필요한 혹은 프로젝트를 구동하기 위해 npm install을 통해서 패키지를 설치하면 package.json에서 이를 관리해준다. 프로젝트의 의존성과 스크립트, 각종 설정을 정의하는 중요한 역할을 해주면서 동시에 프로젝트의 구조와 특성을 파악할 수 있다.
나의 경우도 그렇지만 초기 설정 이후로는 사실 이 package.json을 자주 보지 않는 것 같다. 주로 사용하는 필드만 사용하고 나머지 필드는 잘 모르거나 아예 건드리지 않았던 것 같다. 명령어 같은 경우에도 보통 자주 사용하는 것만 사용하고 다른 명령어는 아예 건드리지 않는 것 같다.
node_modules 같은 경우에도 패키지를 저장하는 공간으로만 인식하고, 프로젝트의 성능을 개선하는 데 직접적으로 무언가를 해본 경험은 없었던 것 같다. 책에서 이런 점들을 구체적으로 설명해준다고 해서 한번 정리해보려고 한다.
package.json 톺아보기
package.json이라는 파일을 우리가 항상 사용하고 있지만, 해당 파일의 정의를 명확하게 기억하고 있지는 않은 것 같다.
package.json은 모든 자바스크립트 프로젝트의 설정과 의존성 관리, 실행 스크립트를 정의하는 역할을 한다. 지금까지 프로젝트를 했을 때를 돌아보면, 초기 설정 단계에서만 확인을 하고 이후에는 package.json을 확실히 잘 열어보는 경우가 없는 것 같다.
package.json을 제대로 파보면 Node.js로 진행되는 프로젝트의 정보를 기술하는 파일로, 프로젝트의 메타데이터를 정의하고, 패키지 실행과 개발에 필요한 의존성을 나열하며, 실행 가능한 스크립트를 설정하는 역할을 한다. 이름에서도 볼 수 있듯이 JSON 형식으로 작성해줘야 한다.
JSON으로 적는 이유도 갑자기 궁금하긴 한데 책을 읽다보면 나오지 않을까 싶다. JSON으로 적었을 때 은근히 제약이 생기는 부분(예: 주석을 적는다거나)이 있었는데 그에 대해서도 추가로 알려준다고 한다.
package.json의 주요 필드 완전 정복
이번에 책을 통해 package.json의 필드들을 이렇게 디테일하게 본 것이 처음이었다. 평소에 name, version, scripts, dependencies 정도만 보고 넘어갔는데, 실제로는 정말 많은 필드들이 있고 각각 중요한 역할을 한다는 걸 알게 되었다.
1. name과 scope
name 필드는 Node.js 프로젝트의 이름을 선언하는 필드다. 흥미롭게도 필수적인 필드는 아니다. npm 레지스트리에 업로드하거나 내부적으로 해당 프로젝트를 다른 곳에서 참조할 목적이 없다면 없어도 된다.
{
"name": "my-awesome-project"
}
웹 서비스 목적으로 만든 경우에는 보통 name 필드가 필요 없다. 특정 서버에 업로드해서 사용하기 때문이다. 하지만 npm에 업로드할 시에는 고유한 명칭을 정해야 한다.
scope는 @scope/packagename 형태로 사용된다. @tanstack/react-query, @babel/core 같은 패키지들을 많이 봤을 것이다. npm의 각 사용자나 조직은 자신만의 스코프를 가질 수 있고, 타 사용자는 중복되는 스코프를 사용할 수 없다.
{
"name": "@mycompany/ui-components"
}
2. version
name과 마찬가지로 웹 서비스를 제공하는 용도라면 상관없지만, npm 레지스트리에 업로드해야 한다면 신경써야 한다. name과 version의 조합은 항상 레지스트리 내부에서 고유해야 하기 때문이다.
{
"name": "my-package",
"version": "1.2.3"
}
3. description, keywords, homepage
이 필드들은 패키지에 대한 기본 정보를 제공한다.
{
"description": "A lightweight utility library for React applications",
"keywords": ["react", "utility", "hooks", "typescript"],
"homepage": "https://github.com/username/my-package"
}
npm info "package명"으로 이 정보들을 확인할 수 있다.
4. license
패키지에 대한 라이선스를 기재하는 곳이다. 개발자로서 알아두면 좋은 주요 라이선스들:
- MIT: 제한이 느슨한 편으로, 오픈소스로 배포할 의무가 없어서 개발자들이 라이선스 걱정 없이 안전하게 사용할 수 있다.
- ISC:
npm init으로 빈 프로젝트를 만들면 기본값으로 설정된다. - BSD: 버클리 캘리포니아 대학에서 만들었으며, 상업적 이용이 가능하지만 저작권자의 이름과 라이선스 내용을 함께 배포해야 한다.
{
"license": "MIT"
}
5. files
패키지를 npm 레지스트리에 업로드할 때 포함해야 할 파일 목록을 선언할 수 있다. 이를 잘 활용하면 패키지를 의존성으로 설치할 때 필요한 파일만 선택적으로 배포할 수 있어 패키지 크기를 줄이는 데 도움이 된다.
{
"files": [
"dist/",
"src/",
"README.md",
"package.json"
]
}
.gitignore와 유사한 문법으로 선언하면 되고, .npmignore 파일로도 관리할 수 있다.
6. main, browser, bin
main은 패키지의 진입 파일을 의미한다. 설정되어 있지 않다면 index.js를 진입점으로 사용한다.
{
"main": "./dist/index.js"
}
browser는 모듈을 브라우저와 같은 클라이언트에서 사용한다면 이 필드를 사용한다. webpack 같은 번들러가 이 필드를 참조한다.
bin은 실행 가능한 파일을 가진 패키지에서 사용한다. create-react-app이 좋은 예시다:
{
"name": "create-react-app",
"bin": {
"create-react-app": "./index.js"
}
}
7. scripts
package.json에서 가장 자주 쓰이는 필드 중 하나다. npm에서 기본적으로 제공하는 명령어뿐만 아니라 임의의 명령어를 선언해서 사용할 수 있다.
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx",
"test": "jest",
"deploy": "npm run build && npm run upload"
}
}
npm run 명령어 또는 npm run-script 명령어로 실행할 수 있고, scripts의 명령어로 다른 scripts를 실행하는 것도 가능하다.
8. engines, os, cpu
패키지가 실행 가능한 환경을 제한할 때 사용한다.
{
"engines": {
"node": ">=14.0.0",
"npm": ">=6.0.0"
},
"os": ["darwin", "linux"],
"cpu": ["x64", "ia32"]
}
9. type
CommonJS와 ESModule 중 Node.js가 어떤 모듈 형식을 사용할지 알리는 필드다.
{
"type": "module"
}
선언하지 않으면 commonjs로 기본 선택된다.
10. exports와 imports
exports는 main의 대안으로 패키지의 진입점을 나타낸다.
{
"exports": {
".": "./index.js",
"./utils": "./src/utils.js",
"./components": "./src/components.js"
}
}
imports는 해당 패키지 내부에서만 쓸 수 있는 구문으로, #으로 시작해야 한다.
{
"imports": {
"#utils/*": "./src/utils/*.js",
"#components": "./src/components/index.js"
}
}
package.json 생성과 설정
npm init으로 시작하기
package.json을 생성하는 가장 쉬운 방법은 npm init이다. 몇 가지 질문과 함께 빠르게 생성할 수 있다.
주석을 넣고 싶다면 scripts, dependencies 안을 제외하고 //을 사용해서 주석 처리를 할 수 있다. @접두사와 _comment도 가능하다.
{
"//": "이것은 주석입니다",
"_comment": "이것도 주석으로 활용할 수 있습니다",
"name": "my-project"
}
.npmrc 파일 이해하기
.npmrc 파일은 npm과 관련된 설정 값을 가진다. 아래 4곳에 위치할 수 있다:
- 프로젝트 최상위:
/path/to/my/project/.npmrc - 사용자 홈 디렉터리:
~/.npmrc - 글로벌 구성 파일:
$PREFIX/etc/.npmrc - npm 내장 구성 파일:
/path/to/npm/.npmrc
파일 내의 설정은 모두 키=값 형태로 저장된다.
dependencies 완전 이해하기
버전 관리의 핵심
npm에서 패키지 버전을 관리할 때 사용하는 특수 기호들을 제대로 이해하는 것이 중요하다.
{
"dependencies": {
"react": "18.0.0", // 정확히 이 버전만
"lodash": "^4.17.21", // 4.17.21 이상 5.0.0 미만
"moment": "~2.29.4", // 2.29.x 버전만
"debug": "*" // 아무 버전이나
}
}
- ^(캐럿): 해당 버전과 호환되는 버전까지 허용
- ~(틸드): 패치 버전의 변경까지만 허용
- *: 아무 버전이나 상관없음
dependencies vs devDependencies vs peerDependencies
dependencies
npm 패키지를 이용하거나 개발하기 위해 반드시 필요한 패키지들이다.
{
"dependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0",
"axios": "^1.3.0"
}
}
devDependencies
개발할 때만 필요한 패키지들이다. 사용자의 관점이 아닌 개발자의 관점에서 필요한 도구들이다.
{
"devDependencies": {
"webpack": "^5.0.0",
"@babel/core": "^7.0.0",
"typescript": "^4.9.0",
"jest": "^29.0.0",
"eslint": "^8.0.0"
}
}
peerDependencies
실제로 써본 기억이 별로 없었는데, npm 패키지를 만들거나 배포할 때 중요하다고 한다. 주로 두 가지 용도로 사용된다:
- 호환성 선언: 어떤 패키지와 호환되는지 명시적으로 표시
- 사용자에게 특정 패키지 설치 주의 알림: 무거운 라이브러리를 강제로 설치하게 하지 않고 사용자가 선택할 수 있게 함
{
"name": "react-use",
"peerDependencies": {
"react": "*",
"react-dom": "*"
}
}
peerDependenciesMeta
peerDependencies에 선언해둔 패키지를 굳이 설치하지 않아도 에러를 발생시키지 않겠다는 조건을 명시한다.
{
"peerDependencies": {
"koa": "*",
"express": "*",
"nestjs": "*"
},
"peerDependenciesMeta": {
"koa": { "optional": true },
"express": { "optional": true },
"nestjs": { "optional": true }
}
}
npm 주요 명령어 정리
개발하면서 자주 사용하는 명령어들을 정리해보았다. 책에서는 npm run에 대해서만 다뤘지만, 실제 개발에서 유용한 다른 명령어들도 함께 정리해보겠다.
npm run의 동작 원리
책에서 언급한 npm run에 대해 좀 더 자세히 알아보자. npm run은 npm을 사용할 때 가장 많이 사용하는 명령어 중 하나다. 뒤에 오는 명령어를 package.json에서 찾아서 실행하는 역할을 한다.
인자를 전달하고 싶으면 npm run dev -- --port=3000 이런 식으로 작성할 수 있다. 여기서 중요한 점은 -- 이후의 모든 내용이 실제 명령어에 인자로 전달된다는 것이다.
흥미로운 점은 scripts에 "eslint ."이라고 해두고 lint로 실행할 때, 실제로 우리가 직접 eslint를 실행하면 안 될 수도 있다는 것이다. 그 이유는 npm run이 실행할 때 node_modules를 참고해서 eslint를 찾아서 실행하기 때문이다. 즉, 로컬에 설치된 패키지를 우선적으로 찾아서 실행한다.
마치며
이번에 package.json과 npm의 기본 개념들을 정리하면서 정말 많은 것들을 새롭게 알게 되었다. 특히 package.json의 다양한 필드들과 그 의미를 이해하고 나니, 다른 프로젝트의 package.json을 볼 때도 훨씬 더 많은 정보를 읽어낼 수 있게 되었다.
peerDependencies나 exports, imports 같은 필드들은 라이브러리를 만들 때 특히 유용할 것 같고, engines나 os 같은 필드들은 환경 제약이 있는 프로젝트에서 활용할 수 있을 것 같다.
npm 명령어들도 평소에 사용하던 것들 외에 npm audit, npm ci, npm dedupe 같은 명령어들을 새로 알게 되었는데, 특히 성능 최적화나 보안 관련해서 유용할 것 같다.
다음 장에서는 실제로 npm install을 실행했을 때 어떤 일들이 벌어지는지, 그리고 node_modules의 구조에 대해서 더 자세히 다룬다고 하니 기대가 된다. 기본기를 탄탄히 다져가는 과정이 생각보다 재미있고 유익하다는 걸 다시 한번 느끼고 있다.
'DEV > FE' 카테고리의 다른 글
| Next.js + MDX 블로그 만들기 - 1 (0) | 2025.05.31 |
|---|---|
| React Native + Expo로 ToDo 만들어보기 (1) | 2025.05.24 |
| 네이버 2025 공채 1차 면접까지 회고 (7) | 2025.05.17 |
| Tanstack Table 쓰기 전에 공식문서 읽어보기(번역과 함께) (0) | 2025.04.04 |