"package.json은 단순한 설정 파일이 아냐. 런타임, 패키지 관리자, 번들러, 편집기가 저마다 필요한 약속을 여기서 읽어."
프로젝트의 신분증부터 읽어
package.json을 보면 이 프로젝트가 누구인지, 어떤 Node에서 실행되는지, 밖으로 무엇을 공개하는지 알 수 있어. 필드가 많아 보여도 먼저 확인할 순서는 분명해.
name은 패키지 이름이야. 레지스트리에 공개하려면 필요하고, 범위를 붙인 패키지는@scope/name처럼 적어.version은 시맨틱 버전 문자열이야. 공개 패키지라면 반드시 있어야 해.type은.js파일을 ESM으로 읽을지 CommonJS로 읽을지 정해. 새 프로젝트에서 가장 먼저 확인할 필드 중 하나야.main은 주로 예전 CommonJS 소비자를 위한 진입점이야. 새 패키지는 공개 경로를exports로 더 정확히 제한해.exports는 소비자가 가져올 수 있는 경로와 조건을 선언해. ESM, CommonJS, 타입 선언을 같은 공개 경로 아래에서 나눌 수 있어.scripts에는 빌드, 테스트, 실행처럼 반복할 명령에 이름을 붙여 둬.- 네 가지 의존성 필드는 런타임용, 개발용, 호스트 제공용, 선택 설치용 패키지를 구분해. 다음 수업에서 자세히 다뤄.
engines는 지원하는 Node 버전 범위를 알려 줘. 패키지 관리자 설정에 따라 경고하거나 설치를 막을 수 있어.files는 공개할 압축 파일에 넣을 경로를 좁혀 줘. 공개 전에npm pack --dry-run으로 실제 목록까지 확인하는 게 안전해.
exports로 공개 경로를 정해
exports는 단순히 첫 파일 하나를 가리키는 필드가 아냐. 패키지의 공개 경계를 만들고, 같은 경로를 ESM과 CommonJS 소비자에게 알맞은 파일로 연결해.
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./utils": {
"types": "./dist/utils.d.ts",
"import": "./dist/utils.mjs",
"require": "./dist/utils.cjs"
}
}
import { x } from 'my-pkg'에는 . 아래의 import 조건이 쓰여. require('my-pkg/utils')에는 ./utils 아래의 require 조건이 쓰이고. 더 중요한 점도 있어. exports에 내놓지 않은 내부 경로는 소비자가 함부로 가져갈 수 없어. 공개 API를 파일 배치와 분리하는 셈이지.
버전 숫자는 호환성 약속이야
시맨틱 버전은
MAJOR.MINOR.PATCH로 읽어.- MAJOR는 기존 사용법이 깨지는 변경이야.
- MINOR는 이전 사용법을 유지하면서 기능을 더한 버전이야.
- PATCH는 공개 API를 바꾸지 않는 수정이야.
^1.2.3은 보통 2.0.0 전까지, ~1.2.3은 보통 1.3.0 전까지 허용해. 1.2.3은 그 버전 하나만 가리켜. 범위는 업데이트 정책이고, 잠금 파일은 실제 설치 결과를 고정하는 기록이야. 둘은 서로 대신하지 않아.scripts는 팀이 공유하는 실행 표면이야
npm run build는 scripts.build에 적힌 명령을 실행해. 뒤쪽 명령에 인자를 넘길 때는 npm run build -- --flag처럼 구분자를 둬. 인자 없이 npm run만 실행하면 프로젝트가 제공하는 스크립트 목록을 볼 수 있어.
스크립트를 실행할 때는 node_modules/.bin이 경로에 들어와. 그래서 "build": "vite build"처럼 적어도 전역 Vite를 따로 설치할 필요가 없어. 이 작은 규칙 덕분에 누구나 같은 프로젝트 버전의 도구를 실행해.
Pippa의 고백
예전에는 템플릿의
package.json을 읽지도 않고 복사했어. 아빠가 각 필드가 누구와 맺는 계약인지 설명해 보라니까 막히더라. 그 뒤로는 새 프로젝트를 만들 때 exports, engines, files부터 확인해. 이 파일은 잡다한 설정 모음이 아니라 프로젝트가 바깥세상에 내미는 신분증이니까.