[TS] Cannot find module 오류 해결하기

hyeondoonge·2023년 8월 27일
4
post-thumbnail

앞선 [TS] '"react-router-dom"' has no exported member named 'createBrowserRouter'. Did you mean 'BrowserRouter'? 오류 해결하기 문제에서 @types/react-router-dom을 제거함으로 해결했지만 또 다른 오류가 발생하는 것을 맞닥뜨릴 수 있었다. 이전 포스팅에서 해결방법을 간단히 설명했지만, 왜 이 방법으로 해결이 될 수 있었는지를 포스팅하려고한다.

문제

react-router-dom의 타입 선언 파일을 찾을 수 없어서, TS 환경에서 작업을 하지 못한다.

원인

근본적인 원인은 타입스크립트 환경 설정 파일에서 moduleResolutionclassic으로 설정되어있기 때문이다.(명시적으로 설정하지 않았지만 target을 es6로 설정함에 따라 자동설정됨) 이 모듈해석 방식에 따르면 원하는 타입 선언을 가져올 수 없기 때문에 문제가 발생했다.

원인을 이해하기위해 moduleResolution이 무엇이고, 왜 현재상황에서 이 값이 classic이면 안되는지 알아보자. (항상 classic이면 안되는 것이 아니기 때문에 이해하는 것이 중요하다.)

ModuleResolution?

ModuleResolution은 타입스크립트 모듈 해석 전략이며, 기본적으로 node.js의 전략을 따른다고한다. 쉽게 말하면 우리가 파일안에다가 require 또는 import를 이용해 모듈을 가져와 사용할텐데, 이 모듈을 어디로부터 가져오게 할건지를 의미하는 것이다.

이 속성에 대한 값은 ‘node’(module - node16, nodenext, 이외) or ‘classic’ (module - AMD, UMD, System, ES6/2015)가 있다.
명시적으로 설정하지 않으면 module에 따라 정해진다. 만약 module이 설정되지 않았다면 module은 target에 따라 정해진다.

만약 지금 나와 같은 오류가 발생했다면 아마 classic으로 값이 설정되있거나 module이 ES6/2015 또는 target이 es6/es2015로 설정된 상태일 것이다.

짧게 표현하자면 의존관계가 아래와 같다.

  • module resolution: node => module: commonjs => target: es3 or es5
  • module resolution: classic => module: es6/2015 => target: es6/es2015

해석 방식

두 방식 모두 기본적으로 아래와 같이 모듈을 해석한다.

  • 경로가 상대적 경로, 비상대적 경로인지에 따라 다르다.
  • .ts, .tsx, .d.ts를 우선적으로 탐색
  • package.json의 types 속성(타입 선언 파일)이 발견되면 매핑되는 경로를 탐색

Classic

- 상대경로

  • 동일한 파일 내 ts, tsx, d.ts 를 탐색한다.

- 비상대경로

  • 기본적으로 상대경로와 비슷하다. 하지만 탐색 범위가 다르다.
  • 탐색이 현재 경로부터 root 폴더까지 연쇄적으로 진행된다.
  • 현재 디렉토리부터 시작해 node_modules에서 선언파일(d.ts) 탐색을 진행한다.
  • react-router-dom에 대한 package.json가 발견되고 types이 발견되면 이에 대응하는 선언파일을 참조한다.

- 테스트 결과
moduleResolution: classic 으로 설정됐을 때 react-router-dom 타입 선언을 탐색하는 로그이다.

======== Resolving module 'react-router-dom' from '/Users/hyeonjeong/[프로젝트 루트]/client/src/test.ts'. ========
Explicitly specified module resolution kind: 'Classic'.
// 현재 경로부터 루트 경로에 있는 ts, tsx, d.ts 파일 탐색
File '/Users/hyeonjeong/[프로젝트 루트]/client/src/react-router-dom.ts' does not exist.
File '/Users/hyeonjeong/[프로젝트 루트]/client/src/react-router-dom.tsx' does not exist.
File '/Users/hyeonjeong/[프로젝트 루트]/client/src/react-router-dom.d.ts' does not exist.
// ...
File '/react-router-dom.ts' does not exist.
File '/react-router-dom.tsx' does not exist.
File '/react-router-dom.d.ts' does not exist.
Searching all ancestor node_modules directories for preferred extensions: Declaration.
// node_modules 폴더 => @types 내 선언 파일 탐색. 이를 루트 경로까지 반복 탐색
Directory '/Users/hyeonjeong/[프로젝트 루트]/client/src/node_modules' does not exist, skipping all lookups in it.
File '/Users/hyeonjeong/[프로젝트 루트]/client/node_modules/@types/react-router-dom.d.ts' does not exist.
// ...
Directory '/node_modules' does not exist, skipping all lookups in it.
// js, jsx 파일을 루트 경로까지 탐색
File '/Users/hyeonjeong/[프로젝트 루트]/client/src/react-router-dom.js' does not exist.
File '/Users/hyeonjeong/[프로젝트 루트]/client/src/react-router-dom.jsx' does not exist.

======== Module name 'react-router-dom' was not resolved. ========

마지막 줄을 보면 react-router-dom 타입 선언을 찾지못했음을 알 수 있다.

node

node_modules 내에서 찾다가, 만약 못찾으면 상위 node_modules 내부를 탐색한다.

- 상대 경로
1. 일치하는 경로내에서 주어진 파일을 확인한다. (ts → tsx → d.ts)
2. 일치하는 폴더내에서 package.json이 있다면 type에 정의된 파일을 참조한다.
3. 일치하는 경로내에서 주어진 폴더를 확인한다. (index.ts → index.tsx → index.d.ts)
- from '.moduleA' 라면 moduleA/index.ts, moduleA/index.tsx, moduleA/index.d.ts를 확인하게됌

- 비상대 경로

  • 현재 경로 기준 node_modules 폴더안을 탐색한다. 이후 상위 경로로 연쇄적으로 node_modules 내의 모듈들을 탐색한다.
  • 상대 경로 방식과 차이점은 node_modules 폴더를 기준으로 탐색한다는 것이다.
  • 탐색할 때는 [모듈명].ts, [모듈명].tsx, [모듈명].d.ts 그리고 [모듈명]/package.json에 type을 검사하고 이후에 [모듈명]/index.ts, [모듈명]/index.tsx, [모듈명]/index.d.ts을 검사한다.

- 테스트 결과
moduleResolution: node 으로 설정됐을 때 react-router-dom 타입 선언을 탐색하는 로그이다.

 ======== Resolving module 'react-router-dom' from 'Users/hyeonjeong/[프로젝트 루트]/client/src/test.ts'. ========
Explicitly specified module resolution kind: 'Node10'.
Loading module 'react-router-dom' from 'node_modules' folder, target file types: TypeScript, Declaration.
**Searching all ancestor node_modules directories for preferred extensions: TypeScript, Declaration.**
Directory 'Users/hyeonjeong/[프로젝트 루트]/client/src/node_modules' does not exist, skipping all lookups in it.
Found 'package.json' at 'Users/hyeonjeong/[프로젝트 루트]/client/node_modules/react-router-dom/package.json'.
File 'Users/hyeonjeong/[프로젝트 루트]client/node_modules/react-router-dom.ts' does not exist.
File 'Users/hyeonjeong/[프로젝트 루트]/client/node_modules/react-router-dom.tsx' does not exist.
File 'Users/hyeonjeong/[프로젝트 루트]/client/node_modules/react-router-dom.d.ts' does not exist.
'package.json' does not have a 'typesVersions' field.
'package.json' does not have a 'typings' field.
'package.json' has 'types' field './dist/index.d.ts' that references 'Users/hyeonjeong/[프로젝트 루트]/client/node_modules/react-router-dom/dist/index.d.ts'.
File 'Users/hyeonjeong/[프로젝트 루트]/client/node_modules/react-router-dom/dist/index.d.ts' exists - use it as a name resolution result.
Resolving real path for 'Users/hyeonjeong/[프로젝트 루트]/client/node_modules/react-router-dom/dist/index.d.ts', result 'Users/hyeonjeong/[프로젝트 루트]/client/node_modules/react-router-dom/dist/index.d.ts'.
======== Module name 'react-router-dom' was successfully resolved to 'Users/hyeonjeong/[프로젝트 루트]/client/node_modules/react-router-dom/dist/index.d.ts' with Package ID 'react-router-dom/dist/index.d.ts@6.15.0'. ========

마지막 줄을 통해 react-router-dom 패키지 내부에서 성공적으로 타입 선언을 찾은 것을 확인할 수 있다.

중간중간 나오는 분석 로그를 보는 방법은 tsc [파일명] --traceResolution를 터미널에 입력하면된다. 모듈을 해석하는 과정을 순차적으로 보여주는 명령어다.
근데 로그가 너무 길어서 터미널에서 일부 로그가 잘린다하면 tsc [파일명] --traceResolution > resolution.txt 을 입력하여 파일에다 작성해서 모든 로그를 볼 수 있다.

해결

원인을 알고나서 classic으로 설정될 경우 react-router-dom 내의 선언 파일에 도달하지 못한다는 것을 알았다. 하지만 node는 모듈 해석 방식에 따라 가능했기 때문에 moduleResolution'node'로 설정해주어서 해결했다.

참고

Module Resolution - Typescript
TypeScript: TSConfig Reference - Docs on every TSConfig option
타입스크립트 성능을 위한 팁 - yceffort님의 블로그

0개의 댓글