게시글

5만명이 선택한 평균 별점 4.9의 제로초 프로그래밍 강좌! 로드맵만 따라오면 됩니다! 클릭
강좌29 - NodeJS - 10달 전 등록

package.json에 추가된 exports 필드(속성)

요즘 노드 생태계에서 ESMCommonJS의 싸움이 한창입니다. 슬슬 표준인 ESM으로 넘어가고 있기는 하지만 아직 저마다의 사정으로 CommonJS를 고수하고 있기도 합니다. 대표적으로 서버 프레임워크로 많이 쓰는 Nest.js가 아직 CommonJS를 고수하고 있고 ESM으로 이전할 계획은 아직 없다고 합니다.

다행히 요즘 많은 라이브러리들이 CommonJS와 ESM을 동시에 지원하고 있습니다. 내 프로젝트가 ESM이라면 라이브러리의 ESM 모듈(ESM의 M이 모듈이라서 동어 반복이긴 하지만 그냥 ESM 모듈이라고 하겠습니다)을 불러오고, 내 프로젝트가 CommonJS라면 라이브러리의 CommonJS 모듈을 불러오는 셈이죠. 그런데 내 프로젝트가 어떻게 해당 라이브러리의 올바른 모듈을 불러올 수 있는 것일까요? 

바로 package.json에 추가한 exports 필드(속성)를 보고 불러오는 것입니다. 참고로 내 프로젝트를 ESM으로 만들고 싶다면 이 게시글 을 읽어보세요.

기존에는 package.json의 main 필드를 주로 참고했습니다. main 필드에 적힌 경로의 파일이 제일 중요한 파일이었기 때문이죠. 하지만 ESM 모듈과 CommonJS 모듈 두 개를 동시에 지원하는 경우에는 main 필드에 두 파일을 모두 적을 수가 없습니다. 그래서 나온 게 exports 필드라고 보시면 됩니다. exports 필드가 있으면 main, module, types 등보다 exports 필드를 더 우선시합니다.

유명한 axios 라이브러리의 package.json을 한 번 보시죠.

  "exports": {
    ".": {
      "types": {
        "require": "./index.d.cts",
        "default": "./index.d.ts"
      },
      "browser": {
        "require": "./dist/browser/axios.cjs",
        "default": "./index.js"
      },
      "default": {
        "require": "./dist/node/axios.cjs",
        "default": "./index.js"
      }
    },
    "./lib/adapters/http.js": "./lib/adapters/http.js",
    "./lib/adapters/xhr.js": "./lib/adapters/xhr.js",
    "./unsafe/*": "./lib/*",
    "./unsafe/core/settle.js": "./lib/core/settle.js",
    "./unsafe/core/buildFullPath.js": "./lib/core/buildFullPath.js",
    "./unsafe/helpers/isAbsoluteURL.js": "./lib/helpers/isAbsoluteURL.js",
    "./unsafe/helpers/buildURL.js": "./lib/helpers/buildURL.js",
    "./unsafe/helpers/combineURLs.js": "./lib/helpers/combineURLs.js",
    "./unsafe/adapters/http.js": "./lib/adapters/http.js",
    "./unsafe/adapters/xhr.js": "./lib/adapters/xhr.js",
    "./unsafe/utils.js": "./lib/utils.js",
    "./package.json": "./package.json"
  },

exports 필드가 위와 같이 적혀져 있습니다. 일단 .(점)은 axios를 의미합니다. 즉 import 'axios'나 require('axios')를 할 때 어떤 파일을 불러올 것인지를 적어둔 것이라 보시면 됩니다. 이 부분은 복잡하니까 잠깐 넘어가고 아래 './lib/adapters/http.js'의 경우에는 import 'axios/lib/adapters/http.js'를 했을 때 './lib/adapters/http.js' 파일을 불러온다는 것을 알 수 있습니다. ESM이든 CommonJS든 불러오는 파일이 동일한 경우에는 이렇게 간단하게 적을 수 있습니다.

이제 다시 .(점)을 봅시다. 점 아래에는 types, browser, default가 키가 있습니다. 이들은 조건입니다. 조건에 대한 리스트는 여기 에 있습니다. types는 타입스크립트에서 타입을 불러올 때, browser는 브라우저에서 사용하는 경우, default는 그 외 기본적인 경우를 의미합니다. default 속성은 반드시 객체에서 제일 마지막에 있어야 합니다. 각각의 조건에서 다시 아래 require과 default가 있습니다. 이들도 조건이라고 보면 됩니다. require는 CommonJS에서 불러올 때 참조할 파일이고, default는 그 외의 경우 불러올 파일입니다.

      "types": {
        "require": "./index.d.cts",
        "default": "./index.d.ts"
      },

즉, types+require는 CommonJS 타입스크립트 환경에서 타입을 찾으려고 할 때 참조할 파일을 적은 것이고 index.d.cts를 참조하라고 되어 있습니다. cts 확장자가 낯설다면 CommonJS인 ts 파일을 명시적으로 가리키는 것이라고 보면 됩니다(ESM 확장자인 mts와 상응) types+default는 CommonJS가 아닌 타입스크립트 환경(ESM이 되겠죠)에서 타입을 찾으려고 할 때 참조할 파일을 적은 것입니다. index.d.ts를 보라고 되어 있습니다.

index.d.cts와 index.d.ts를 각각 살펴보면 확실히 각자 모듈에 맞게 다르게 타이핑되어 있습니다.

index.d.cts

...
declare const axios: axios.AxiosStatic;

export = axios;

index.d.ts

...
declare const axios: AxiosStatic;

export default axios;

axios 깃헙에서 browser와 default의 파일도 한 번 확인해보세요. browser+require는 CommonJS 브라우저 코드, browser+default는 ESM 브라우저 코드, default+require는 node의 CommonJS 코드, default+default는 node의 ESM 코드입니다. 첫 번째 default는 types도, browser도 아닌 상황을 의미하는 것이고, 두 번째 default는 require이 아닌 상황(CommonJS가 아닌 상황)이므로 서로 다른 default인 것입니다. browser 코드에는 노드 모듈들을 불러오는 부분이 제외되어 있습니다.

참고로 다음과 같이 development, production처럼 process.env.NODE_ENV 별로 다른 파일을 가져오게 할 수도 있습니다.(웹팩 설명에서 발췌)

{
  "type": "module",
  "exports": {
    "node": {
      "development": {
        "module": "./index-with-devtools.js",
        "import": "./wrapper-with-devtools.js",
        "require": "./index-with-devtools.cjs"
      },
      "production": {
        "module": "./index-optimized.js",
        "import": "./wrapper-optimized.js",
        "require": "./index-optimized.cjs"
      },
      "default": "./wrapper-process-env.cjs"
    },
    "development": "./index-with-devtools.js",
    "production": "./index-optimized.js",
    "default": "./index-optimized.js"
  }
}

이렇게 명시를 해놓으면 import나 require할 때 올바르게 가져올 수 있습니다. 하지만 가장 좋은 것은 빨리 모든 생태계가 ESM으로 통일돼서 이런 상황이 없어지는 것이겠죠 ㅠㅠ 그런데 여전히 production vs development, browser vs node 같은 상황은 존재하므로 exports 필드는 모듈 생태계 통합 이후에도 존재할 겁니다.

다음 글에서는 CommonJS 환경에서 ESM 모듈을 불러오는 방법에 대해 알아보겠습니다.

조회수:
0
목록
투표로 게시글에 관해 피드백을 해주시면 게시글 수정 시 반영됩니다. 오류가 있다면 어떤 부분에 오류가 있는지도 알려주세요! 잘못된 정보가 퍼져나가지 않도록 도와주세요.
Copyright 2016- . 무단 전재 및 재배포 금지. 출처 표기 시 인용 가능.
5만명이 선택한 평균 별점 4.9의 제로초 프로그래밍 강좌! 로드맵만 따라오면 됩니다! 클릭

댓글

아직 댓글이 없습니다.