package.json 진입점 설정과 타입 접근성의 관계 이해하기

InseoYangInseoYang
4 min read

JavaScript와 TypeScript 패키지를 개발할 때 package.json의 진입점 설정은 단순히 어떤 파일이 먼저 로드될지를 결정하는 것 이상의 의미를 갖는다. 특히 TypeScript를 사용하는 프로젝트에서는 타입 접근성에 중요한 영향을 미친다.

회사 패키지를 수정하다가, “@패키지/dist/…” 와 같은 임포트 경로가 너무 프로젝트 내부의 구조등을 대놓고 보여주고 있기도 하고, 무엇보다 보기에 거슬려서 이를 해결하기 위해 type을 따로 관리하는 진입점을 만들었다. 그랬더니 “Cannot find type definition file for ‘@패키지’” , “Cannot find type definition file for ‘컴포넌트’”같은 오류가 나기 시작했다. 나는 분명히 여기(미리 정의해둔 d.ts 파일)에 있는 타입을 가져다 쓰라고 명시했는데, 왜 그런걸까?

package.json 진입점의 기본 개념

Node.js에서 패키지를 로드할 때는 두 단계로 진행된다. 먼저 올바른 디렉토리를 찾고, 그 다음 디렉토리 내에서 진입점 파일을 찾는다. 진입점은 require() 또는 import 호출의 반환값이 되는 파일이다.

전통적으로 Node.js는 package.json의 main 필드를 사용해 패키지의 진입점을 결정했다.

{
  "name": "my-package",
  "main": "dist/index.js"
}

하지만 현대적인 JavaScript 생태계에서는 더 다양한 진입점 필드가 사용된다.

{
  "name": "my-package",
  "main": "dist/index.js",       // CommonJS 환경용
  "module": "dist/esm/index.js", // ESM 지원 번들러용
  "types": "dist/index.d.ts",    // TypeScript 타입 정의용
  "browser": "dist/browser.js"   // 브라우저 환경용
}

진입점 설정이 타입 접근성에 미치는 영향

TypeScript 프로젝트에서 package.json에 types 필드를 설정하면 기존에 잘 임포트되던 것들이 안되기 시작한다.(ex. “@패키지/dist/constants/어쩌구”) 이전에는 패키지 내부의 모든 타입에 접근할 수 있었지만, 이제는 진입점 파일에서 명시적으로 export한 타입만 접근 가능해지기 때문이다.

왜 세부 경로의 타입에 접근할 수 없게 되는가?

types 필드를 설정하면 TypeScript 컴파일러는 해당 파일을 패키지의 공식 타입 정의 진입점으로 간주한다.

이로써

  1. 컴파일러는 지정된 타입 진입점 파일만 참조

  2. 진입점에서 export되지 않은 타입은 공개 API로 간주되지 않음

  3. 결과적으로 import { SomeType } from 'package/path/to/internal/types'와 같은 세부 경로 임포트가 불가능해짐 < 이 글을 쓰게 된 이유다.

이는 의도적인 설계로, 패키지의 API 경계를 명확히 정의하고 내부 구현 세부사항을 캡슐화하기 위함이다.

Node.js의 "exports" 필드와 패키지 캡슐화

최신 Node.js는 exports 필드를 통해 더 강력한 캡슐화 메커니즘을 제공한다.

{
  "exports": {
    ".": "./dist/index.js",
    "./utils": "./dist/utils.js"
  }
}

exports 필드는 main 필드보다 우선순위가 높으며, 명시적으로 정의된 진입점 외에는 모든 접근을 차단한다. 이는 패키지 작성자가 공개 API를 명확하게 정의할 수 있게 해준다.

예를 들어, 위 설정에서는:

  • import pkg from 'pkg-a' - 허용됨 (메인 진입점)

  • import utils from 'pkg-a/utils' - 허용됨 (명시적으로 정의됨)

  • import internal from 'pkg-a/internal' - 차단됨 (정의되지 않음)

이 특성은 패키지의 내부 구현을 보호하고 안정적인 API를 유지하는 데 도움이 된다고 생각한다.

타입 접근성 문제 해결 방법

그렇다면 우리는 모든 타입을 따로 export 하여 진입점을 일일이 설정해주어야할까? 그러기엔 프로젝트 사이즈 등등 조금 무리가 있어보인다. 타입 접근성 제한으로 인한 문제를 해결하는 방법은 여러 가지가 있다.

1. 다시 내보내기(Re-export)

가장 간단한 방법은 진입점 파일에서 필요한 타입을 다시 내보내는 것이다.

// index.ts (진입점)
export { SomeType } from './internal/types';

2. TypeScript 경로 매핑

TypeScript 사용자를 위해 typesVersions 필드를 사용하여 세부 경로에 대한 타입 정의를 제공할 수 있다.

{
  "typesVersions": {
    "*": {
      "utils": ["./dist/utils.d.ts"],
      "internal/*": ["./dist/internal/*.d.ts"]
    }
  }
}

3. exports 필드 확장

exports 필드를 사용하여 타입과 함께 하위 모듈을 명시적으로 노출할 수 있다.

{
  "exports": {
    ".": {
      "import": "./dist/esm/index.js",
      "require": "./dist/cjs/index.js",
      "types": "./dist/index.d.ts"
    },
    "./utils": {
      "import": "./dist/esm/utils.js",
      "require": "./dist/cjs/utils.js",
      "types": "./dist/utils.d.ts"
    }
  }
}

진입점 설정의 추가적인 이점

진입점을 명확하게 설정하는 것은 타입 접근성 외에도 다음과 같은 여러 이점이 있다.

번들 크기 최적화

ESM 형식의 진입점(module 필드)을 제공하면 번들러가 트리 쉐이킹을 더 효과적으로 수행할 수 있다. 이는 최종 번들 크기를 줄이는 데 도움이 된다.

환경별 최적화

다양한 환경(Node.js, 브라우저, ESM, CommonJS 등)에 맞는 최적화된 번들을 제공할 수 있다.

{
  "exports": {
    ".": {
      "node": "./dist/node.js",
      "browser": "./dist/browser.js",
      "default": "./dist/index.js"
    }
  }
}

API 안정성 향상

명확한 진입점 정의는 패키지의 공개 API를 명확히 하고, 내부 구현 세부사항을 변경하더라도 호환성을 유지하는 데 도움이 된다.

정리하며

package.json의 진입점 설정은 단순한 기술적 세부사항이 아니라 패키지의 아키텍처와 API 설계에 중요한 영향을 미친다고 생각한다. 특히 TypeScript를 사용하는 프로젝트에서는 타입 접근성에 직접적인 영향을 주므로, 패키지 개발 초기 단계에서 신중하게 계획하는 것이 좋을 것 같다. 나는 이미 많이 개발해 둔 프로젝트에 뒤늦게 적용하다가 결국 이 글을 쓰게 됐다.

진입점을 적절히 설정함으로써 패키지의 공개 API를 명확히 정의하고, 내부 구현을 보호하며, 다양한 환경에서의 최적화를 제공할 수 있다. 이는 패키지의 유지보수성, 성능, 사용자 경험을 모두 향상시키는 중요한 요소라고 생각한다.

현대적인 JavaScript/TypeScript 패키지를 개발할 때는 exports 필드를 활용하여 더 세밀한 진입점 제어와 강력한 캡슐화를 구현할 수 있다.

0
Subscribe to my newsletter

Read articles from InseoYang directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

InseoYang
InseoYang