자바스크립트 개발자를 위한 package.json 파일 정리

자바스크립트 개발자를 위한 package.json 파일 정리

자바스크립트 프로젝트의 최상위 경로를 열어 보면 대부분 package.json 파일이 있습니다. 처음에는 설치한 패키지와 실행 명령어를 적어 두는 파일 정도로 보입니다. 그런데 프로젝트가 커질수록 낯선 필드가 하나둘 늘어나는데요. 어떤 프로젝트에는 exports가 있고 다른 프로젝트에는 module이 있습니다. 모노레포(monorepo)를 열어 보면 workspaces도 등장하죠.

이 필드들이 모두 같은 프로그램을 위한 설정은 아닙니다. npm과 같은 패키지 관리자(package manager)가 읽는 필드도 있고 Node.js와 Bun 같은 런타임(runtime)이 읽는 필드도 있습니다. TypeScript나 번들러(bundler)가 사실상 표준처럼 사용하는 필드도 빼놓을 수 없고요. 도구와 버전에 따라 지원 범위까지 달라서 최신 문서만 보면 오래된 프로젝트의 package.json을 이해하기 어려울 때도 있죠.

이번 글에서는 자주 사용하는 필드를 읽는 주체와 목적에 따라 나눠 살펴보겠습니다. 현재 권장되는 설정뿐 아니라 예전 프로젝트에서 마주칠 수 있는 필드와 버전별 차이도 함께 다룹니다.

package.json은 누가 읽을까?

package.json을 npm 전용 설정 파일이라고 생각하기 쉽지만 실제로는 여러 도구가 함께 읽는 프로젝트 설명서에 가깝습니다. 사람뿐 아니라 지속적 통합(Continuous Integration, CI) 환경도 이 파일을 읽습니다.

읽는 주체주된 목적대표 필드
패키지 관리자설치, 스크립트 실행, 작업 공간 관리dependencies, scripts, workspaces
런타임과 모듈 로더모듈 형식과 진입점(entry point) 결정type, main, exports, imports
패키지 저장소검색, 발행, 배포 파일 구성name, version, files, publishConfig
저장소 개발자와 CI개발 도구와 실행 환경 통일devDependencies, devEngines, packageManager
TypeScript와 번들러타입과 플랫폼별 빌드 결과 선택types, module, browser, sideEffects
ESLint, Prettier 같은 도구해당 도구의 프로젝트 설정 불러오기prettier, browserslist, lint-staged

어떤 필드가 npm의 package.json 공식 문서에 없다고 해서 잘못된 설정은 아닙니다. 그 필드를 npm이 아닌 다른 도구가 읽을 수도 있기 때문인데요. 반대로 이름만 보고 동작을 추측해서도 안 됩니다. 같은 목적처럼 보이는 enginesdevEngines도 대상과 데이터 구조가 완전히 다릅니다.

name, version과 기본 정보

nameversion은 패키지의 고유한 이름과 버전을 나타냅니다. npm 저장소에 발행할 때는 둘 다 필수지만 외부에 발행하지 않는 애플리케이션에서는 생략할 수 있습니다. 다만 name은 작업 공간(workspace)에서 패키지를 식별하거나 exports와 함께 자기 패키지를 참조할 때도 쓰이므로 비공개 프로젝트에서도 의미가 있을 수 있습니다.

package.json
{
  "name": "@acme/format-date",
  "version": "2.1.0",
  "description": "날짜를 읽기 쉬운 문자열로 변환하는 유틸리티",
  "keywords": ["date", "format", "i18n"]
}

패키지 검색과 유지 보수에 필요한 정보도 함께 적을 수 있습니다.

  • homepage: 프로젝트 홈페이지나 문서의 URL입니다.
  • repository: Git 저장소의 위치입니다.
  • bugs: 버그를 제보할 이슈 트래커나 이메일 주소입니다.
  • license: MIT, Apache-2.0 같은 SPDX 라이선스 식별자입니다.
  • author, contributors: 저자 한 명과 공헌자 여러 명을 기록합니다.
  • funding: 후원 페이지를 연결하며 npm fund가 이 값을 사용합니다.
package.json
{
  "homepage": "https://example.com/format-date",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/acme/format-date.git"
  },
  "bugs": "https://github.com/acme/format-date/issues",
  "license": "MIT",
  "author": "Dale Seo <dale@example.com>",
  "funding": "https://github.com/sponsors/acme"
}

github:acme/format-date처럼 짧게 쓴 저장소 주소도 오래전부터 인식됩니다. 다만 최신 npm은 발행 과정에서 위와 같은 객체 형태로 정규화하고 경고할 수 있습니다.

오래된 패키지에서는 객체나 배열 형태의 licenses를 볼 수도 있는데요. 새로 작성하는 패키지는 SPDX 식별자를 담은 단일 license 필드를 사용하는 편이 좋습니다.

scripts

scripts에는 개발, 빌드, 테스트처럼 반복해서 실행하는 명령어를 등록합니다. 스크립트 이름은 대부분 자유롭게 정할 수 있으며, bun run buildnpm run build처럼 실행합니다.

package.json
{
  "scripts": {
    "dev": "astro dev",
    "build": "astro build",
    "typecheck": "astro check && tsc --noEmit",
    "test": "playwright test"
  }
}

starttest처럼 패키지 관리자가 특별히 다루는 이름도 있고, prepost 접두사를 이용한 실행 전후 훅도 있습니다. 예를 들어 npm은 test 앞에 pretest, 뒤에 posttest가 있으면 차례대로 실행합니다. 패키지 관리자마다 생명 주기 스크립트(lifecycle script)의 지원 범위와 실행 시점이 조금씩 다르므로 발행이나 설치 과정에 연결할 때는 사용하는 도구의 문서를 확인해야 합니다.

예전 프로젝트에서 자주 보이는 prepublish에는 현재 npm에서 사용 중단(deprecated) 표시가 붙어 있습니다. 이름과 달리 npm publish가 아니라 npm installnpm ci에서 실행되는 역사적인 동작 때문에 혼란이 컸는데요. npm 4부터는 패키징과 로컬 설치 전에 실행할 작업에는 prepare, 발행할 때만 실행할 작업에는 prepublishOnly를 사용합니다. 정확한 실행 순서는 npm 생명 주기 스크립트 문서에서 확인할 수 있습니다. 더 자세한 실행 방법은 package.json 스크립트 활용법에서 살펴볼 수 있습니다.

dependencies와 devDependencies

dependencies는 패키지나 애플리케이션이 실행될 때 필요한 의존성을 나타냅니다. 반면 devDependencies에는 테스트 도구, 린터(linter), 포맷터(formatter)처럼 저장소를 개발하거나 빌드할 때만 필요한 패키지를 넣습니다.

package.json
{
  "dependencies": {
    "zod": "^4.0.0"
  },
  "devDependencies": {
    "prettier": "^3.0.0",
    "typescript": "^6.0.0",
    "vitest": "^4.0.0"
  }
}

Bun에서는 bun add zodbun add --dev typescript로 각각 추가할 수 있습니다. npm을 사용한다면 npm install zodnpm install --save-dev typescript가 같은 역할을 합니다. 직접 JSON을 편집할 수도 있지만 패키지 관리자를 통하면 잠금 파일(lockfile)까지 함께 갱신되므로 명령어를 사용하는 편이 안전합니다.

여기서 “개발할 때 쓰면 무조건 devDependencies”라고 외우면 곤란합니다. 라이브러리가 공개한 타입 선언 파일에서 다른 @types 패키지를 참조한다면 소비자에게도 그 타입이 필요하므로 해당 패키지를 dependencies에 넣어야 할 수 있습니다. 반면 애플리케이션의 타입 검사에만 쓰는 @types 패키지는 보통 devDependencies가 맞습니다.

버전 앞의 ^, ~와 같은 범위 문법은 유의적 버전에서 자세히 다룹니다. 범위 안에서 실제로 선택된 버전을 고정하는 방법은 자바스크립트 패키지 잠금 파일을 참고하세요.

peerDependencies와 peerDependenciesMeta

peerDependencies는 내 패키지가 직접 소유하기보다 소비자의 프로젝트와 함께 사용해야 하는 호스트 패키지(host package)의 호환 범위를 나타냅니다. React 플러그인(plugin)이나 ESLint 플러그인처럼 특정 프레임워크(framework)를 확장하는 패키지에서 많이 사용합니다.

package.json
{
  "peerDependencies": {
    "react": ">=18 <20",
    "react-dom": ">=18 <20"
  }
}

이 필드는 npm 버전에 따라 체감 동작이 달랐습니다. npm 3부터 6까지는 피어 의존성(peer dependency)을 자동으로 설치하지 않고 누락이나 충돌을 경고했습니다. npm 7부터는 기본적으로 함께 설치합니다. 오래된 CI나 잠금 파일을 다룰 때 설치 결과가 다른 이유가 여기에 있을 수 있습니다.

지원할 수 있는 호스트가 여러 개라서 일부 피어 의존성을 선택 사항으로 두고 싶다면 peerDependenciesMeta를 사용합니다.

package.json
{
  "peerDependencies": {
    "react": ">=18",
    "react-dom": ">=18"
  },
  "peerDependenciesMeta": {
    "react-dom": {
      "optional": true
    }
  }
}

optionalDependencies와 bundleDependencies

optionalDependencies에 등록한 패키지는 내려받거나 빌드하지 못해도 전체 설치가 실패하지 않습니다. 운영체제별 네이티브(native) 기능이나 없어도 기본 기능은 동작하는 성능 최적화 패키지에 어울립니다. 설치되지 않은 경우는 애플리케이션 코드가 직접 처리해야 합니다.

package.json
{
  "optionalDependencies": {
    "fsevents": "^2.3.3"
  }
}

bundleDependenciesnpm pack으로 만드는 압축 파일 안에 지정한 의존성까지 포함합니다. 인터넷에 연결되지 않은 환경에 패키지를 전달하는 등의 특별한 상황에서 쓰며, 일반적인 라이브러리에서는 드뭅니다. 과거부터 사용된 bundledDependencies라는 철자도 같은 의미로 계속 지원됩니다.

overrides와 resolutions

직접 설치한 패키지가 아니라 그 아래의 전이 의존성(transitive dependency) 버전을 바꿔야 할 때가 있습니다. 보안 취약점이 수정된 버전을 강제하거나 중복 버전을 하나로 맞출 때 npm은 overrides를 사용합니다.

package.json
{
  "overrides": {
    "lodash": "^4.17.21"
  }
}

overrides는 루트 package.json에 있을 때만 npm의 의존성 해석에 반영됩니다. npm에는 8.3부터 추가되었기 때문에 그보다 오래된 npm에서는 이 설정에 의존할 수 없습니다.

Yarn 생태계에서는 같은 목적으로 resolutions 필드를 먼저 사용해 왔습니다. 현재 Bun은 두 필드를 모두 지원합니다. 다만 패키지 관리자마다 중첩 의존성을 지정하는 문법에는 차이가 있습니다. 여러 관리자를 지원해야 한다면 한 관리자의 필드만 보고 같은 결과가 나온다고 가정해서는 안 됩니다.

engines

engines는 패키지를 설치하고 실행하는 소비자에게 필요한 런타임이나 패키지 관리자 버전을 나타냅니다. 대표적으로 지원하는 Node.js와 npm 버전 범위를 명시합니다.

package.json
{
  "engines": {
    "node": ">=20",
    "npm": ">=10"
  }
}

이름이 강제 규칙처럼 보이지만 npm에서는 기본적으로 권고 사항입니다. 소비자가 범위에 맞지 않는 버전을 사용하면 경고하고 engine-strict 설정을 켠 경우에만 설치를 거부합니다. engines만 추가했다고 운영 환경의 버전이 자동으로 바뀌거나 CI가 반드시 실패하는 것은 아닙니다.

패키지가 특정 플랫폼에서만 동작한다면 os, cpu, libc로 범위를 더 좁힐 수 있습니다. osdarwin, linux, win32 같은 process.platform 값, cpux64, arm64 같은 process.arch 값을 사용합니다. libc는 Linux에서 glibcmusl 구현을 구분할 때 사용합니다.

package.json
{
  "os": ["linux"],
  "cpu": ["x64", "arm64"],
  "libc": "glibc"
}

이 필드들은 네이티브 실행 파일을 배포하는 패키지에는 중요하지만 순수 자바스크립트 패키지에 습관적으로 넣을 필요는 없습니다.

devEngines와 packageManager

engines가 패키지 소비자의 실행 환경을 설명한다면 devEngines는 소스 저장소에서 작업하는 개발자와 CI의 도구를 검사합니다. 현재 npm은 install, ci, run 명령을 실행하기 전에 이 필드를 확인합니다.

두 필드는 이름이 비슷하지만 값의 구조부터 다릅니다.

필드대상값의 형태npm의 기본 동작
engines패키지를 설치하는 소비자버전 범위 문자열경고
devEngines저장소 개발자와 CIname, version, onFail 객체오류, 경고 또는 무시
packageManager저장소에서 사용할 패키지 관리자이름@정확한 버전 문자열지원 도구에 따라 다름
package.json
{
  "packageManager": "npm@11.6.2",
  "engines": {
    "node": ">=20"
  },
  "devEngines": {
    "runtime": {
      "name": "node",
      "version": "^20.17.0 || >=22.9.0",
      "onFail": "error"
    },
    "packageManager": {
      "name": "npm",
      "version": "^11",
      "onFail": "error"
    }
  }
}

onFail에는 error, warn, ignore를 지정할 수 있고 생략하면 error처럼 동작합니다. 현재 npm CLI는 런타임 이름으로 node, 패키지 관리자 이름으로 npm을 예상하므로 devEngines.packageManager.name에 무조건 bun이나 pnpm을 넣는 방식은 이식성이 없습니다. devEngines를 모르는 오래된 패키지 관리자는 이 검사 자체를 하지 않습니다.

최상위 packageManager는 저장소에서 사용할 패키지 관리자와 정확한 버전을 선언하는 별개의 필드입니다. Corepack은 이 값을 읽어 npm, Yarn, pnpm 버전을 선택하고 다른 관리자를 실행하는 실수를 막을 수 있습니다. 다만 Corepack은 Node.js 25부터 기본 배포에 포함되지 않고, 기본 지원 관리자에도 Bun은 들어 있지 않습니다. Bun 프로젝트라면 packageManager를 설명용 메타데이터(metadata)로만 믿지 말고 mise 같은 버전 관리자나 CI의 oven-sh/setup-bun 버전도 함께 고정하는 편이 안전합니다.

workspaces

workspaces는 하나의 저장소에서 여러 패키지를 관리하는 모노레포(monorepo)의 위치를 지정합니다. 패키지 관리자는 각 작업 공간을 찾아 설치하고 루트의 node_modules에 연결합니다.

package.json
{
  "name": "acme-monorepo",
  "private": true,
  "workspaces": ["apps/*", "packages/*"]
}

루트 패키지는 보통 발행 대상이 아니므로 private: true를 함께 둡니다. Yarn은 오래전부터 작업 공간을 지원했고 npm은 v7부터 공식 지원하기 시작했습니다. 오래된 npm에서는 같은 package.json으로 설치해도 작업 공간이 연결되지 않을 수 있습니다. workspace:* 같은 내부 의존성 표기법의 지원 범위도 패키지 관리자와 버전에 따라 다릅니다. 호환해야 할 조합에서 직접 검증해 봐야 합니다.

type

type은 가장 가까운 package.json 아래에 있는 .js 파일을 Node.js가 어떤 모듈 형식으로 해석할지 정합니다. "module"이면 ES 모듈(ECMAScript Module, ESM), "commonjs"이거나 필드가 없으면 기본적으로 CommonJS로 해석합니다.

package.json
{
  "type": "module"
}

이 설정이 모든 확장자를 바꾸는 것은 아닙니다. .mjstype과 관계없이 ES 모듈이고 .cjs는 항상 CommonJS입니다. Node.js는 모호한 .js 파일의 문법을 감지할 수도 있지만 명시적인 type을 두면 불필요한 감지 비용과 도구별 해석 차이를 줄일 수 있습니다.

type은 Node.js 12에서 도입되었습니다. 그보다 오래된 Node.js까지 지원해야 하는 패키지라면 .mjs.cjs 확장자, 트랜스파일 결과물, 다음에 설명할 main을 함께 고려해야 합니다. Node.js에서 두 모듈 형식을 사용하는 방법은 Node.js의 ES 모듈에서 더 자세히 다룹니다.

main과 exports

main은 패키지 이름만 가져왔을 때 불러올 기본 진입점입니다. 오랫동안 사용된 표준 필드이고 지금도 폐기되지 않았습니다. 값을 생략하면 전통적으로 루트의 index.js를 기본값으로 사용합니다.

package.json
{
  "main": "./dist/index.cjs"
}

exports는 Node.js 12.7에서 추가된 현대적인 진입점 설정입니다. 기본 진입점뿐 아니라 공개할 하위 경로와 import, require, 실행 환경에 따른 조건부 진입점까지 정의할 수 있습니다. Node.js가 둘을 모두 지원하는 버전이라면 exportsmain보다 우선합니다.

package.json
{
  "main": "./dist/index.cjs",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./format": {
      "import": "./dist/format.js",
      "require": "./dist/format.cjs"
    }
  }
}

새 패키지는 exports를 중심으로 외부에 공개할 경로를 설계하는 것이 좋습니다. 오래된 Node.js와 번들러를 지원하려면 같은 기본 진입점을 가리키는 main을 남겨 둘 수 있습니다. main만 있는 오래된 패키지도 여전히 유효합니다.

기존 패키지에 exports를 추가할 때는 특히 조심해야 합니다. 이 필드가 생기면 명시하지 않은 하위 경로를 더 이상 가져올 수 없기 때문에 소비자가 사용하던 my-package/internal.js 같은 깊은 가져오기(deep import)가 갑자기 실패할 수 있습니다. 기존에 공개했던 경로를 모두 포함하지 않고 exports를 추가하는 변경은 주 버전(major version)을 올릴 만한 호환성 파괴 변경입니다. 세부 규칙은 Node.js 패키지 진입점 문서를 참고하세요.

imports

imports는 패키지 내부에서만 사용하는 경로 별칭을 정의합니다. 외부 소비자에게 공개하는 exports와 달리 현재 패키지의 코드가 긴 상대 경로를 피하거나 환경별 구현을 선택할 때 사용합니다. 키는 반드시 #으로 시작해야 합니다.

package.json
{
  "imports": {
    "#config": "./src/config.js",
    "#utils/*": "./src/utils/*.js"
  }
}

imports는 Node.js 12.19와 14.6에서 추가되었습니다. 그보다 오래된 런타임은 이 별칭을 해석하지 못하므로 지원 범위가 넓은 라이브러리라면 빌드 도구에서 경로를 변환하거나 상대 경로를 유지해야 합니다.

types와 typesVersions

types는 TypeScript가 패키지의 대표 타입 선언 파일을 찾을 때 사용하는 필드입니다. npm 자체의 표준 필드는 아니지만 타입이 포함된 패키지에서는 사실상 표준으로 자리 잡았습니다.

package.json
{
  "main": "./dist/index.js",
  "types": "./dist/index.d.ts"
}

오래된 패키지에서 볼 수 있는 typingstypes와 같은 의미로 TypeScript가 계속 지원합니다. 새 패키지에서는 더 짧고 널리 쓰이는 types를 권장합니다.

TypeScript 버전에 따라 서로 다른 선언 파일을 제공해야 한다면 typesVersions를 사용할 수 있습니다.

package.json
{
  "types": "./dist/index.d.ts",
  "typesVersions": {
    "<5.0": {
      "*": ["ts4/*"]
    }
  }
}

최신 TypeScript는 exports 안의 types 조건도 이해하지만 오래된 TypeScript는 최상위 typestypesVersions에 의존합니다. 지원하려는 TypeScript 버전 범위에 따라 둘을 병행할 수 있습니다. 타입 선언을 패키지에 포함하는 방법은 TypeScript 공식 발행 문서DefinitelyTyped 소개를 참고하세요.

module, browser와 sideEffects

module은 npm이나 Node.js가 정의한 표준 필드가 아닙니다. Node.js의 ES 모듈 도입 과정에서 제안되었다가 채택되지는 않았지만 esbuild를 비롯한 주요 번들러main보다 트리 셰이킹(tree shaking)에 유리한 ES 모듈 진입점을 찾는 용도로 지원하면서 사실상 표준이 되었습니다. Bun의 모듈 해석exports가 없을 때 ES 모듈 가져오기에서 module, main 순서로 확인합니다.

browser는 브라우저용 진입점이나 Node.js 전용 모듈의 대체 경로를 번들러에 알려 줍니다. 문자열 하나로 진입점을 지정하거나 객체로 파일별 대체 관계를 작성할 수 있습니다.

sideEffects는 사용하지 않는 모듈을 번들에서 제거해도 되는지 알려 주는 힌트입니다. 모든 파일이 가져오기만으로 외부 상태를 바꾸지 않는다면 false로 둘 수 있지만, 전역 폴리필(polyfill)이나 CSS 가져오기가 있다면 해당 파일을 배열로 남겨야 합니다.

package.json
{
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "browser": "./dist/index.browser.js",
  "sideEffects": ["**/*.css", "./dist/polyfill.js"]
}

새 패키지에서는 조건부 exports로 Node.js, 브라우저, import, require 진입점을 더 명확하게 나눌 수 있습니다. 그렇다고 modulebrowser가 곧바로 폐기된 것은 아닙니다. 오래된 번들러와 폭넓게 호환해야 하는 라이브러리는 exports와 함께 이 필드들을 유지하기도 합니다. 중요한 점은 module이 브라우저용이라는 뜻이 아니라 ES 모듈 형식이라는 점입니다.

sideEffects: false는 단순한 성능 옵션이 아니라 코드 제거를 허용하는 계약입니다. 잘못 지정하면 필요한 초기화 코드가 배포용 빌드(production build)에서 사라질 수 있으므로 webpack의 트리 셰이킹 문서처럼 실제 사용하는 번들러의 기준을 확인해야 합니다.

files, private과 publishConfig

files는 npm에 발행할 압축 파일에 포함할 경로를 허용 목록(allowlist)으로 지정합니다. 생략하면 대부분의 프로젝트 파일이 포함되며 .git, node_modules, 잠금 파일처럼 npm이 항상 제외하는 항목도 있습니다.

package.json
{
  "files": ["dist"]
}

READMELICENSE, package.json처럼 npm이 항상 넣어 주는 파일은 목록에 다시 적지 않아도 됩니다. 실제 포함 목록은 발행 전에 npm pack --dry-run으로 확인하는 것이 안전합니다. 빌드 결과물이나 타입 선언을 빠뜨리거나 테스트용 인증 정보를 실수로 포함하는 문제를 미리 찾을 수 있습니다.

발행하면 안 되는 애플리케이션이나 모노레포 루트에는 private: true를 설정합니다. npm은 이 값이 있으면 발행을 거부하므로 공개 저장소인지와 관계없이 실수로 발행하는 일을 막아 줍니다.

publishConfig는 발행할 때만 적용할 저장소, 공개 범위, 배포 태그 등을 덮어씁니다.

package.json
{
  "private": false,
  "publishConfig": {
    "registry": "https://npm.pkg.github.com",
    "access": "public",
    "tag": "next"
  }
}

패키지를 처음 발행한다면 npm 패키지 배포 방법도 함께 살펴보세요.

bin

bin은 패키지를 설치했을 때 터미널에서 실행할 명령어와 실제 파일을 연결합니다. 명령줄 인터페이스(Command-Line Interface, CLI) 도구를 발행할 때 사용합니다.

package.json
{
  "name": "hello-cli",
  "bin": {
    "hello": "./bin/hello.js"
  }
}

실행 파일의 첫 줄에는 보통 #!/usr/bin/env node 같은 셔뱅(shebang)을 넣고 실행 권한도 부여해야 합니다. 패키지를 설치하면 npm은 이 파일을 node_modules/.bin에 연결하므로 스크립트나 npm exec, bunx에서 명령어 이름으로 실행할 수 있습니다.

도구별 비표준 설정

많은 개발 도구가 별도 설정 파일뿐 아니라 package.json 안의 전용 필드도 읽습니다. 작은 프로젝트에서는 설정 파일 수를 줄일 수 있어 편리합니다.

package.json
{
  "prettier": {
    "semi": false
  },
  "browserslist": [">0.5%", "not dead"],
  "lint-staged": {
    "*.{js,ts}": "eslint --fix"
  }
}

이러한 필드는 JSON 문법상 문제가 없지만 npm이 의미를 보장하지는 않습니다. Prettier, Browserslist, lint-staged가 각자 자신의 필드를 읽는 방식입니다. Bun의 trustedDependencies, pnpm의 pnpm, Yarn의 installConfig처럼 특정 패키지 관리자만 이해하는 필드도 같은 범주에 속합니다.

예전 글이나 프로젝트에서는 eslintConfig도 자주 보입니다. 이 필드는 ESLint의 기존 eslintrc 설정 체계에서는 유효하지만 현재 기본인 플랫 설정(flat config)은 package.json 설정을 지원하지 않습니다. 최신 ESLint에서는 eslint.config.js 같은 별도 파일로 옮겨야 하며, 구버전 ESLint를 유지하는 프로젝트라면 기존 eslintConfig를 당장 지울 필요는 없습니다. ESLint의 설정 마이그레이션 문서에서 차이를 확인할 수 있습니다.

프로젝트가 커지면 도구별 설정이 한 파일에 뒤섞이고 변경 충돌도 잦아집니다. 설정의 양이 많거나 JavaScript로 조건을 표현해야 한다면 각 도구의 전용 설정 파일로 분리하는 편이 관리하기 쉽습니다.

오래된 package.json을 읽을 때

package.json은 오래된 필드가 한 번에 사라지기보다 새 필드와 오랫동안 공존하는 경향이 있습니다. 기존 프로젝트를 볼 때는 아래처럼 해석하면 됩니다.

오래된 프로젝트에서 보이는 모습현재의 해석과 대응
main만 있음여전히 유효합니다. 오래된 런타임까지 지원하는 가장 넓은 진입점입니다.
mainmodule이 함께 있음CommonJS와 번들러용 ES 모듈을 나눈 관행입니다.
exports가 없음잘못된 것은 아니지만 공개 하위 경로를 제한하거나 조건부 진입점을 만들 수 없습니다.
typings가 있음TypeScript가 계속 지원하는 types의 동의어입니다.
prepublish 스크립트가 있음사용 중단 표시가 붙은 필드입니다. 목적에 따라 prepareprepublishOnly로 옮깁니다.
peerDependencies가 설치되지 않음npm 3부터 6의 정상 동작일 수 있습니다. npm 7부터 기본 설치됩니다.
resolutions만 있음Yarn 계열 설정일 가능성이 큽니다. npm은 overrides를 사용합니다.
eslintConfig가 있음eslintrc 기반 ESLint 설정입니다. 플랫 설정에서는 별도 파일이 필요합니다.
devEngines가 동작하지 않음사용하는 패키지 관리자가 지원하지 않거나 버전이 오래되었을 수 있습니다.

라이브러리라면 “현재 내 개발 환경에서 동작한다”는 것만으로 충분하지 않습니다. 지원한다고 약속한 가장 오래된 Node.js, npm, TypeScript, 번들러 조합에서도 설치와 가져오기를 시험해야 합니다. 반대로 사내 애플리케이션처럼 실행 환경을 완전히 통제할 수 있다면 불필요한 레거시 필드를 걷어 내고 exports, devEngines, 고정된 도구 버전으로 설정을 단순화할 수 있습니다.

마치며

package.json은 단순한 의존성 목록이 아니라 패키지 관리자, 런타임, 저장소, 타입 검사기(type checker), 번들러가 함께 읽는 계약서입니다. 어떤 필드를 사용할지는 프로젝트가 애플리케이션인지 라이브러리인지, 어디에 발행하는지, 어느 버전까지 지원하는지에 따라 달라집니다.

새 프로젝트에서는 exports, types, packageManager처럼 의도를 분명히 드러내는 필드를 우선하되, 오래된 소비자를 지원해야 한다면 main, module, 최상위 types 같은 호환 필드를 함께 유지할 수 있습니다. 낯선 필드를 발견했을 때는 npm 문서에 있는지만 확인하지 말고 어느 도구가 읽는지와 해당 도구의 버전까지 살펴보는 습관이 중요합니다.

This work is licensed under CC BY 4.0CCBY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord