문제의 배경
일전에 UI 컴포넌트 라이브러리를 만들어 배포하는 프로젝트를 진행하였습니다. CommonJS와 ESM 두 모듈시스템을 지원해주기 위해서 Rollup과 package.json 설정해주었는데요. (아래 포스팅에서 자세히 볼 수 있습니다.)
[UI 컴포넌트 라이브러리] 2. 라이브러리 배포하고 사용하기 (NPM, Github Packages, Docker)
[UI 컴포넌트 라이브러리] 1. Typescript로 React 컴포넌트 만들고 Rollup으로 빌드하기 💡 스토리북의 디자인시스템 튜토리얼을 진행하고 오시는 것을 추천합니다. 튜토리얼을 진행하고 오셨다면 예
mitchellglobal.tistory.com
배포된 컴포넌트 라이브러리를 실제 프로젝트에서 적용해보는데에서 문제를 발견하게 됩니다.
UI 컴포넌트의 package.json
{
"name": "PACKAGE_NAME",
"version": "0.0.1",
"private": false,
"description": "UI component library",
"repository": "YOUR_GITHUB_REPOSITORY",
"author": "YOUR_NAME"
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"types": "dist/esm/@types/index.d.ts",
...
}
main과 module 필드에 각각 CommonJS와 ESM을 위한 경로를 입력하여서 두 모듈시스템에 잘 대응되도록 설정해두었습니다.
프로젝트에서 컴포넌트 하나를 불러와 실행해보니 브라우저에서는 에러 없이 잘 렌더링이 되었습니다.
에러상황
그러나 라이브러리에서 불러온 페이지에 대한 테스트를 작성하고 실행했을 때 "g is not a funcion"이라는 TypeError와 함께 테스트는 실패하게 됩니다.

테스트 config에는 기본적으로 node_modules 하위 폴더에 대한 테스트를 제외하도록 되어있는데도 라이브러리와 관련된 에러가 테스트케이스에 걸려서 FAIL을 주고 있습니다. 게다가 라이브러리 코드에서 "TypeError: g is not a function"과 관련된 에러가 존재하는지 파악하였으나 그런 에러는 없었죠.
도대체 왜? 테스트환경에서만 저런 에러가 발생하는걸까요?
에러의 원인
라이브러리를 사용하는 프로젝트에서는 Vite를 사용하여 빌드하고 있고, 테스트 프레임워크는 Vite와 호환이 잘 되는 Vitest를 사용하고 있었습니다. 그리고 Vite와 Vitest는 ESM을 우선적으로 지원하고 있는 것이 주요한 특징입니다.
Vite로 생성한 React 프로젝트의 package.json
{
"name": "PROJECT_NAME",
"private": true,
"version": "0.0.0",
"type": "module",
...
}
기본적으로 "type"필드가 "module"로 되어 있어 프로젝트의 모든 파일들은 ESM을 따라 작성하게 됩니다.
위에서 본 UI 컴포넌트의 package.json의 설정에 "main" 필드와 "module" 필드에 CommonJS와 ESM에 맞는 경로를 설정하였던 것을 기억하실 겁니다.
{
...
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
...
}
따라서 ESM을 따르는 React 프로젝트 내에서 해당 UI 컴포넌트 라이브러리를 불러와 사용한다면 "module" 필드의 경로를 참조하여 브라우저에서 정상적으로 렌더링이 되는데요.
왜 테스트 환경에서는 에러일까?
"main" 필드의 경로를 "dist/esm/index.js"로 변경하였더니 테스트가 성공하였다는 메시지를 받을 수 있었습니다. Vite와 Vitest가 기본적으로 ESM을 지원하는데도 테스트 환경에서는 "module" 필드를 참조하지 않고 "main" 필드를 참조했다고 추론할 수 있었습니다.
이런 상황에 대해서 Vitest의 Github에서도 Discussion이 열렸었습니다.
How vitest resolves dependency exports · vitest-dev/vitest · Discussion #4233
Hi! so initially I had this issue: I have a node_modules package that exports "main": "dist/cjs/index.js", "module": "dist/esm/index.js", "types": "dist/types/index.d.ts" Where the cjs export had b...
github.com
결국 테스트 환경에서 에러는 UI 컴포넌트 라이브러리의 "module" 필드를 참조하지 못해서 발생한 것입니다.
왜냐하면 테스트환경은 Node 환경이고, Node의 표준 Resolution에 따라 package.json의 "main"을 참조하기 때문입니다.
따라서 "main"과 "module" 필드로 구분하는 방법 말고 다른 방법을 통해 올바른 경로를 참조하게 해야합니다.
exports로 CommonJS, ESM 대응하기
Node.js 공식문서에 의하면 exports필드를 통해 subpath exports와 conditional exports를 지원해줄 수 있다고 하는데요.
여기에서는 conditional exports로 CommonJS는 dist/cjs/index.js로 ESM은 dist/esm/index.js로 조건에 따라 진입점을 다르게 하도록 설정하겠습니다.
수정된 UI 컴포넌트 라이브러리의 package.json
{
...
exports: {
".": {
"require": "./dist/cjs/index.js",
"import": "./dist/esm/index.js"
}
}
...
}
위와 같이 exports를 설정해준다면 "require"로 패키지를 불러오는 경우(CommonJS)에는 "./dist/cjs/index.js"를 참조하여 CommonJS로 빌드된 파일을 참조할 것이고, "import"로 패키지를 불러오는 경우(ESM)에는 "./dist/esm/index.js"를 참조하여 ESM으로 빌드된 파일을 참조하게 됩니다.
변경된 설정으로 다시 배포하고, 프로젝트에서 재설치하여 실행하면 테스트에서도, 브라우저에서도 정상적으로 작동하는 UI 컴포넌트를 만날 수 있게 됩니다.
'JS, TS' 카테고리의 다른 글
LoginPage로 보는 React 통합테스팅(with. msw, testing-library, vitest) (1) | 2023.10.28 |
---|---|
testing-library, vitest로 리액트 Unit test 작성하며 리팩토링하기 (2) | 2023.10.20 |
아이콘은 어떤 확장자로 써야할까? (SVG, PNG, JPEG, GIF, WebP) (0) | 2023.09.20 |
자바스크립트 비트연산자 시프트 활용하기 (Hex to RGB 컬러변환) (0) | 2023.09.08 |
Flutter, Nextjs 하이브리드앱 환경에서의 세션처리와 효율적인 HTTP 통신방법 (Axios 활용) (0) | 2023.07.13 |