내가 만든 npm 패키지가 import가 안되는 이슈 (feat. tsconfig.json & pacakge.json)

김철준·2024년 1월 1일
2

Trouble Shooting

목록 보기
1/1

자주 사용하는 함수 npm 배포

회사 프로젝트들을 하다 보니 프로젝트에서 공통적으로 사용되는 함수들을 있어 이를 라이브러리로 구성하면 어떨까라는 생각을 했다.
그래서 helper,hooks 등 함수들을 라이브러리로 구성하여 npm 배포를 연휴동안 해보았다.

npm으로 배포하는 과정은 여러 레퍼런스들이 있어 이들을 조합하여 그리 어렵지 않게 구성할 수 있었다.

나의 패키지는 react와 typescript로 구성된 라이브러리였으며 vite를 사용하였다.
하지만 배포는 성공적으로 되었으나 내가 설치한 라이브러리가 다른 프로젝트에서 import가 안되는 이슈가 발생했다.

내가 배포한 npm 패키지가 프로젝트에서 import가 안되는 이슈

에러 메시지 : 모듈 'react-useful-utils'에 대한 선언 파일을 찾을 수 없습니다.

처음 몇시간동안 다른 레퍼런스 및 vite 공식문서를 샅샅이 찾아가며 방법들을 찾아해매며 npm 배포만 25번은 했었다.

  • @types/나의 npm 라이브러리 설치
  • vite.config.ts 설정 변경
  • package.json 설정 변경
  • 여러 레퍼런스들을 참조하여 따라해보기

위의 과정을 계속해서 반복했다.

위 과정을 반복하다가 지쳐, 머리를 붙잡고 생각을 해보았다.
문득 드는 생각이 내가 하나의 프로젝트에서만 계속 나의 라이브러리를 설치하여 테스트하고 있었다는 것을 깨달았다.

이슈에 대한 합리적 추론

그러면 다른 프로젝트에서 설치하여 import를 해보자라는 생각이 들어 다른 프로젝트에서 설치하여 import를 해보았다.

(내가 계속해서 import를 진행하던 프로젝트는 create vite(with react,ts) 구성된 프로젝트였다.)

그런데..?!

추론 1 js이면 => import가 된다?

Project 1(react&js using create react app)

정상적으로 import도 되고 사용도 내가 의도한 대로 올바르게 되었다.

그렇다면 이 프로젝트에서는 왜 import가 되는 것일까?
typescript가 아닌 javascript여서 그런 것일까?라는 생각이 들어 타입스크립트 프로젝트에서 import 해보았다.

추론 2 vite로 구성되어있지 않다면 => import가 된다?

Project 2(react&ts using create react app)
그렇다면 프로젝트가 vite로 구성되어있지 않은 프로젝트는 import가 되는 것 아닐까 라는 생각이 들었다.

create react app 명령어를 사용하여 리액트 + 타입스크립트 템플릿으로 프로젝트를 구성하여 나의 패키지를 import해보았다.

성공적으로 import가 되었다.

추론 3 프로젝트가 vite로 구성되어있다면 => import가 안된다.

그러면 vite로 구성된 기존 프로젝트를 다시 들여다보면 여전히 나의 패키지가 import가 안되고 있다.

그렇다면 vite로 구성된 프로젝트에서는 나의 패키지가 왜 안될까 생각을 해보았다.
설정 파일들을 들여다보았다.

추론 3-1 우선 vite 설정 파일에서 차이가 있어 문제가 있는 것이 아닐까하여 import가 되지 않은 프로젝트의 설정 코드를 나의 npm 라이브러리 vite 설정 파일에 덧붙혔다.

import가 안되는 프로젝트의 vite.config.ts

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
})

내 패키지 vite.config.ts

// vite.config.ts
import react from "@vitejs/plugin-react";
import { resolve } from "path";
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";

// https://vitejs.dev/guide/build.html#library-mode
export default defineConfig({
  build: {
    lib: {
      entry: resolve(__dirname, "src/index.ts"),
      name: "react-useful-utils",
      fileName: "react-useful-utils",
    },
  },
  plugins: [dts(), react()],
});

위와 같이 설정하여 다시 패키지를 배포하였으나 여전히 import되지 않았다.
vite 기반인 것이 원인은 아니었던 것이다.

추론 3-2 그렇다면 ts 설정 파일에서 차이가 있어 문제가 있는 것이 아닐까하여 비교해보았다.

import가 안되는 프로젝트의 ts.config.json

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "Bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

내 패키지 tsconfig.json

{
  "compilerOptions": {
    "target": "es5", // 컴파일된 JS 코드의 ECMAScript 버전
    "lib": ["dom", "dom.iterable", "esnext"], // 포함할 라이브러리 목록
    "allowJs": true, // JS 파일 컴파일 허용
    "noEmit": false, // 컴파일러가 출력 파일 생성
    "declaration": true, // .d.ts 선언 파일 생성
    "outDir": "dist", // 컴파일된 파일 저장 디렉토리
    "skipLibCheck": true, // 라이브러리 파일 타입 체크 스킵
    "esModuleInterop": true, // CommonJS 모듈을 ES6처럼 사용
    "allowSyntheticDefaultImports": true, // 디폴트 모듈 import 문법 사용 허용
    "strict": true, // 엄격한 타입 체킹 활성화
    "forceConsistentCasingInFileNames": true, // 파일 이름 대소문자 일관성 강제
    "noFallthroughCasesInSwitch": true, // switch문의 case문 넘어감 방지
    "module": "esnext", // 모듈 시스템 (최신 ECMAScript)
    "moduleResolution": "Node", // 모듈 해석 방식 (Node.js)
    "resolveJsonModule": true, // JSON 모듈 가져오기 허용
    "isolatedModules": true, // 파일을 독립적 모듈로 컴파일
    "jsx": "react-jsx" // JSX 코드 처리 방식 (React JSX)
  },
  // 컴파일할 파일 패턴
  "include": ["./src/**/*.ts"]

}
  • ❌ 탈락 :두 개를 비교해봤을 때,"references": [{ "path": "./tsconfig.node.json" }] 이 코드가 상이하여 문제인가 싶어 패키지 프로젝트에 tsconfig.node.json파일을 추가하고 위 설정 코드를 추가하여 배포하였는데 여전히 import 되지 않았다.

  • ⭕️ 성공 :그러다 눈에 띄는 설정 속성이 있었다
    moduleResolution이라는 속성인데 이는 모듈 해석 방식에 대한 설정이었다.
    뭔가 안되는 이유에 제일 가까울 것 같다는 속성이라는 것이 느껴져 비교해보았다.

일단 내 패키지에서는 "Node"로 설정되어있고 import 실패한 프로젝트에서는 "Bundler"로 값이 설정되어있었다.

그리하여 import 실패한 프로젝트에서 위 설정값을 Node로 변경해보았다.

그랬더니..!

import가 안되는 프로젝트의 moduleResolution 속성 변경한 ts.config.json

{
  "compilerOptions": {
...

    /* Bundler mode */
    "moduleResolution": "Node",
..

...
  },
 ...
}

import가 성공하였다!!

해결성공이다!
그렇다면 moduleResolution이라는 속성은 도대체 무엇이고 "Bundler"로 설정했을 때에는 왜 import가 안되었던 것일까?

moduleResolution이라는 속성은 도대체 무엇이고 "Bundler" 와 "Node"?

moduleResolution이란?

moduleResolution이란 타입스크립트가 모듈들을 해석하는 과정 방식을 설정하는 속성이라고 한다.

"moduleResolution" :"Node"

FROM 공식문서(moduleResolution)

It supports looking up packages from node_modules, loading directory index.js files, and omitting .js extensions in relative module specifiers.

Node로 값을 설정해놓으면 index.js 파일을 로딩하면서 node_modules 디렉터리로부터 패키지들을 검색한다고 한다.

Node 방식은 상대 경로나 절대 경로로 시작하지 않는 모듈 이름을 해석할 때, 먼저 "node_modules" 폴더를 찾아보고, 그 다음으로 상위 폴더로 이동하면서 "node_modules"를 찾는다고 한다.. 이 방식을 사용하면 node_modules 폴더 구조와 package.json 파일을 이용해 모듈을 찾아올 수 있다.

"moduleResolution" :"Bundler"

공식 문서에 따르면 설명을 다음과 같이 하고 있다.

This module resolution mode provides a base algorithm for code targeting a bundler. It supports package.json "exports" and "imports" by default, but can be configured to ignore them. It requires setting module to esnext.
....
the conditions used to resolve package.json "exports" and "imports" are always "types" and "import".

이는 타입스크립트에게 코드가 다른 툴에 의해 번들링될 거라고 알려주고, 이에 맞게 규칙을 완화해준다고 한다.

Node.js의 exports,imports 필드 방식을 지원해주고 ESM imports에 제한되는 부분없이 내보내기 및 들여오기를 커스터마이징을 할 수 있다는 장점이 있는 것 같다.
(사실 Node로 설정해도 큰 문제는 없을 것 같긴 하다.)

힌트?!

Bundler 값에 대해 공식문서에서 더 찾아보면 위 마지막 문장과 같은 힌트를 준다.

the conditions used to resolve package.json "exports" and "imports" are always "types" and "import".

package.json에서 exports"와 "imports" 필드를 사용 하는 조건은 exports 필드에는 types 필드가 있어야하고 imports 필드에는 import 필드가 있어야한다는 힌트를 준다.

공식 문서에서 약간 빈약한 설명을 하긴 했지만 답을 찾을 힌트를 주긴 주었다.

그렇다면 근본적인 해결을 위해 "moduleResolution" :"Bundler"로 설정하여도 import가 될 수 있는 방법을 찾아보자.

🌳 근본적인 해결책(나의 패키지 내 package.json 설정)

위처럼 내 패키지를 사용하는 프로젝트에서의 tsconfig.json에서 moduleResolution:"Node"로 변경해주는 방법이외에 다른 근본적인 방법도 찾았다.

근본적인 방법이란 tsconfig.json에서 moduleResolution:"Bundler"로 설정하여도 import가 될 수 있도록 하는 것이다.

무엇이 문제일까? vscode 오류 확인!

추론한 이유는 다음과 같다.

위와 같이

모듈 'react-useful-utils'에 대한 선언 파일을 찾을 수 없습니다. '/Users/jjalseu/Desktop/projects/my-react-vite-test-app/node_modules/react-useful-utils/dist/react-useful-utils.js'에는 암시적으로 'any' 형식이 포함됩니다.
There are types at '/Users/jjalseu/Desktop/projects/my-react-vite-test-app/node_modules/react-useful-utils/dist/index.d.ts', but this result could not be resolved when respecting package.json "exports". The 'react-useful-utils' library may need to update its package.json or typings.

에러 메시지에서 package.json의 "exports"필드를 살펴보라고 친절하게 알려주고 있었다. index.d.ts를 해석하지 못하는데 이것이 exports 필드에서 문제가 있다고 알려준 것이었다.

그렇다면 답은 나의 배포한 패키지에서 exports 필드를 변경해주는 것이다.
다음은 기존 package.json과 변경된 package.json이다.

Before package.json

{
...
  "type": "module",
  "main": "./dist/react-useful-utils.umd.cjs",
  "module": "./dist/react-useful-utils.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/react-useful-utils.js",
      "require": "./dist/react-useful-utils.umd.cjs",
    }
  },
 ...
}

After package.json

{
...
  "type": "module",
  "main": "./dist/react-useful-utils.umd.cjs",
  "module": "./dist/react-useful-utils.js",
  "exports": {
    ".": {
      "import": "./dist/react-useful-utils.js",
      "require": "./dist/react-useful-utils.umd.cjs",
      "types": "./dist/index.d.ts",

    }
  },
 ...
}

변경된 부분은 exports 필드에 "types": "./dist/index.d.ts"를 추가해주었고 외부에 있는 "types": "./dist/index.d.ts"는 제거해주었다.

원인

문제의 원인은 types 필드가 exports 필드에 포함되어 있지 않아서였다.

TypeScript는 exports 필드가 있다면 exports 필드를 참조하여 모듈의 파일을 찾기 때문에, 선언 파일(.d.ts)이 exports 필드에 포함되어 있지 않으면 TypeScript는 선언 파일을 찾지 못하고 오류를 발생시킬 수 있다고 한다.

이에 따라 exports 필드에 types 필드를 추가하고 진입 선언 파일을 입력해주니 말끔히 해결되었다.

그렇다면 기존에 types 필드에 선언 파일을 설정해주었는데 export가 안된 이유는 무엇일까?

Before package.json

{
...
  "type": "module",
  "main": "./dist/react-useful-utils.umd.cjs",
  "module": "./dist/react-useful-utils.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/react-useful-utils.js",
      "require": "./dist/react-useful-utils.umd.cjs",
    }
  },
 ...
}

package.json에서 types"필드는 다음과 같은 역할을 한다.

"types" 필드는 TypeScript 컴파일러에게 해당 모듈의 타입 선언 파일의 위치를 알려주는 역할을 합니다. 이 필드는 보통 .d.ts 확장자를 가진 파일의 경로를 값으로 가집니다.
타입 선언 파일은 TypeScript 컴파일러가 JavaScript 모듈의 타입 정보를 이해할 수 있도록 돕습니다. 이 파일에는 함수, 클래스, 인터페이스 등의 타입 정보가 포함되어 있습니다. TypeScript는 이 파일을 참조하여 타입 검사를 수행하고, 개발자에게 타입에 관한 도움말을 제공합니다.
따라서, TypeScript로 작성된 프로젝트에서는 각 모듈의 "types" 필드가 매우 중요합니다. 이 필드가 없거나 잘못 설정되어 있다면, TypeScript는 해당 모듈의 타입 정보를 제대로 인식하지 못하고 오류를 발생시킬 수 있습니다.

따라서 기존에는 위와 같이 "types": "./dist/index.d.ts" 설정해주었다.

하지만 "exports" 필드가 추가된다면, Node.js는 이 필드를 우선적으로 참조하게 된다고 한다. 즉, "exports" 필드가 있는 경우, Node.js는 "main", "module", "types" 등의 다른 필드를 무시하고 "exports" 필드만을 참조하게 된다고 한다.

그래서 "exports" 필드가 있는 상황에서는 "types" 필드만 따로 명시해도 TypeScript는 이를 인식하지 못하고, 선언 파일(.d.ts) 위치를 찾지 못하게 된 것이다. 따라서 "exports" 필드 내부에 "types"를 명시해주어야 TypeScript가 선언 파일의 위치를 제대로 인식하게 된다고 한다.

이는 "exports" 필드가 모듈의 내보내기를 더 세밀하게 제어하도록 도입된 기능이기 때문이라고 한다. 이 필드를 사용하면 모듈의 내보내기를 파일 단위로 제어할 수 있으며, ES6 모듈과 CommonJS 모듈을 위한 서로 다른 엔트리 포인트를 지정할 수 있다. 이런 세밀한 제어 기능 때문에, "exports" 필드가 추가되면 이 필드가 모든 내보내기의 역할을 담당하게 되는 것이다.

마무리..

많은 삽집을 통해(연휴 내내 이 문제만 해결해느라 애썼었다..) 결국 근본적인 해결책을 찾았다.
타입스크립트나 패키지 설정 관련해서는 지식이 많이 없어 시행착오를 겪었던 것 같다.

마무리하며 드는 생각은 하루 날 잡고 설정 관련한 스터디를 해봐도 좋을 것 같다는 생각이 들었다. 그래도 이번 이슈는 나름 괜찮은 트러블 슈팅이었다고 생각하고 쬐끔 발전한 느낌이 들어 기분이 좋았다.

  • 하나의 환경이 아닌 여러 환경(프로젝트)에서 테스트해볼 것, 여기서 환경이란 컴퓨터,프로젝트,디바이스 등등이다.
profile
FE DEVELOPER

1개의 댓글

comment-user-thumbnail
2024년 4월 8일

라이브러리 배포 에러의 해답을 찾으러 들렀는데, 문제를 해결해 나가는 과정에서 정말 많이 배우고 갑니다.!

답글 달기