Feb 1, 2026
1 views
subpath 경로를 통해 모듈을 import 했을 때, 런타임에서는 정상 동작하지만 타입은 찾지 못하는 문제가 발생했다. 당연 빌드도 실패했고, 원인을 찾다가 배포도 지연되었다. 이참에 팀이 모듈을 어떻게 관리하고 있는지 파악한 후, 개선할 점이 있다면 제안해보려한다.
import { something } from '@modals/subpath';
// ❌ Cannot find module '@modals/subpath' or its corresponding type declarations.
모듈은 package.json 내에 진입점과 타입을 정의한다. 방식은 두 가지가 있다.
main과 types 필드를 사용하는 방식 (node 12 이전 방식){
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
}
exports 필드를 사용하는 방식 (node 12 이후 방식){
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs"
},
"./subpath": {
"types": "./dist/subpath.d.ts",
"import": "./dist/subpath.mjs"
}
}
}
문제는 런타임(번들러)과 TypeScript는 모듈을 해석하는 방식이 다를수 있다는 점이다.
| 환경 | exports 지원 |
|---|---|
| Vite, webpack 5, esbuild | ✅ |
TypeScript (moduleResolution: "node") | ❌ |
TypeScript (moduleResolution: "bundler") | ✅ |
번들러가 exports 필드를 지원한다면 지정된 subpath 진입점을 통해 모듈을 실행하기 때문에 런타임에서는 정상 동작하지만,
TypeScript 설정이 exports를 지원하지 않는 경우 타입은 subpath 에 지정된 경로를 읽을 수 없고, types 에 지정된 경로만 참조하게 되어 에러가 난다.
| moduleResolution 설정 | exports 지원 | 사용하는 필드 |
|---|---|---|
node | ❌ | main, types |
bundler, node16, nodenext | ✅ | exports 우선 |
우리는 moduleResolution: "node" 로 설정되어있어, TypeScript가 exports 필드를 무시했으며, js 진입점만 exports 필드를 참조하게 되었다.
package.json의 typesVersions 필드를 사용하면 subpath 에 지정된 경로를 읽을 수 있게 된다.
{
"typesVersions": {
"*": {
"subpath": ["./dist/types/subpath.d.ts"]
}
}
}
moduleResolution 을 수정하면 해당 프로젝트에서 import 하는 모듈의 모든 경로가 완전히 변경된다.
우선, 문제가 되는 모듈은 1개이므로 전체 경로를 변경하는 것은 부담스러웠다. 임시 대응 방법으로 typesVersions 필드를 사용할 수 있었다.
typesVersions 은 버전에 따라 타입 경로를 유연하게 지정하는 용도인 듯 했지만, moduleResolution 이 node 인 경우에도 subpath type 파일을 찾을 수 있도록 우회할 수 있다.
typesVersions는 우회 방법일 뿐, 근본적인 해결책은 아니다.
문제점:
exports와 typesVersions를 이중으로 관리해야 하기에, 모듈 제공자 및 사용자 모두 이 시스템을 이해하는데 어려움을 느낄 수 있다.
권장 방법을 찾아보니, 모던 번들러 환경에서는 moduleResolution: "bundler"로 설정하는 것이 런타임과 타입 해석을 일치시키는 가장 깔끔한 방법이었다.
영향 범위 파악
moduleResolution: "bundler" 적용 시 타입 에러가 발생하는 파일 목록 확인exports 없이 main만 있는 오래된 패키지가 있는지 확인팀 내 공유
moduleResolution: "bundler" 전환에 대한 의견 논의