module resolution | 모듈 해석

module resolution은 컴파일러가 무엇을 참고하여 import하는지 찾아내는 과정을 말한다.

컴파일러는 moduleA의 형태가 무엇인지 물어본다. moduleA는 .ts/.tsx 파일 중 하나 일수도 있으며 .d.ts 안에 있는 코드일수도 있다.

먼저 컴파일러는 임포트된 모듈에서 파일을 찾는다. 이렇게 하기 위해서는 컴파일러는 Classic 또는 Node 두 가지 전략중에서 하나를 선택합니다.

이 전략들은 moduleA를 어디서 찾어야하는지 알려줍니다.

만약 모듈 이름이 상대적이지 않다면 컴파일러는 module 선언에 탐색을 시도합니다. 하지만 모듈을 찾을 수 없다면 에러를 남깁니다.

상대적인 vs 상대적이지 않은 모듈 import

모듈 import는 모듈 레퍼런스가 상대적인지 아닌지에 따라 다르게 해석을 합니다.

상대적인 import는 '/', './', '../'으로 시작하는것을 말합니다.

상대적이지 않는 임포트는 "moduleA"와 같습니다.

상대적인 import는 import 하는 파일을 상대적으로 해석합니다. 그리고 module 선언에서 해석할수 없습니다.

만약 상대적인 import를 사용한다면 런타임때 상대적인 위치를 유지하는 것을 보장해야 합니다.

module resolution 전략

Node, Classic 두 가지 전략이 가능하다.--moduleResolution을 사용해서 어떤 전략을 사용할지 명시할 수 있으며

만약 명시하지 않았다면 --module AMD | System | ES2015라면 Classic이 기본값이고 아니라면 Node가 기본값으로 설정이 된다.

Classic

타입스크립트 기본 모듈 해석 전략이다. 이 전략은 주로 이전 버전과의 호환성을 위해 존재합니다.

상대적인 import 는 임포트된 파일을 기준으로 상대적인 import를 해석합니다.

ex) /root/src/folder/A.ts에서 import {b} from './moduleB'을 한다면 다음과 같은 조회를 보여줍니다.

  1. /root/src/folder/moduleB.ts
  2. /root/src/folder/moduleB.d.ts

상대적이지 않은 import인 경우 컴파일러는 임포트 파일이 포함된 디렉토리부터 시작해서 일치하는 정의 파일을 찾아낼려고 합니다.

ex) /root/src/folder/A.ts에서 import {b} from 'moduleB'을 한다면 다음과 같은 위치에서 찾습니다.

  1. /root/src/folder/moduleB.ts
  2. /root/src/folder/moduleB.d.ts
  3. /root/src/moduleB.ts
  4. /root/src/moduleB.d.ts
  5. /root/moduleB.ts
  6. /root/moduleB.d.ts
  7. /moduleB.ts
  8. /moduleB.d.ts

Node

Node.js는 require에 상대경로인지 아닌지에 따라 다르게 행동한다.

ex) /root/src/moduleA.js에서 var x = require('./moduleB')를 한다면 다음과 같은 순서로 해석한다.

  1. /root/src/moduleB.js라는 파일이 존재하는지 확인한다.
  2. /root/src/moduleB 라는 폴더가 있는지 확인하다. 만약 main 모듈이라고 명시된 package.json 파일을 포함하고 있는지 확인한다. /root/src/moduleB/package.json 파일안에 {"main": "lib/mainModule.js"} 을 포함한다면 Nodejs는 /root/src/moduleB/lib/mainModule.js를 참고한다.
  3. /root/src/moduleB라는 폴더가 있는지 확인한다. 만약 index.js 파일을 포함한다면 그 파일은 암시적으로 해당 폴더의 main 모듈로 고려한다.

하지만 상대경로가 이는 이름은 다르게 행동한다.

Node는 node_modules라는 특별한 폴더안에서 찾는다. node_modules 폴더는 현재 파일을 같은 레벨에 있을수 있으며 또는 더 높은 디렉토리 체인에 존재 할수 도 있다.

Node는 디렉토리 체인을 돌면서 각각의 node_module안에서 해당 모듈을 찾을때까지 올라간다.

ex) /root/src/moduleA.js 에서 var a = require('moduleB')

  1. /root/src/node_modules/moduleB.js

  2. /root/src/node_modules/moduleB/package.json (if it specifies a "main" property)

  3. /root/src/node_modules/moduleB/index.js

  4. /root/node_modules/moduleB.js

  5. /root/node_modules/moduleB/package.json (if it specifies a "main" 1. property)

  6. /root/node_modules/moduleB/index.js

  7. /node_modules/moduleB.js

  8. /node_modules/moduleB/package.json (if it specifies a "main" 1. property)

  9. /node_modules/moduleB/index.js

타입스크립트는 어떻게 모듈을 해석하는가

타입스크립트는 컴파일 타임에 모듈을 위한 정의파일을 찾기위해 Nodejs 런타임 해석 전략을 흉내냈다.

이것을 하기위해 타입스크립트는 Nodejs 해석 로직에 타입스크립트 소스 파일 확장자를 덮혀씌웠다.

타입스크립트는 package.json에서 main의 목적을 따라한 types라는 필드를 사용한다.

컴파일러는 main 정의 파일을 찾기 위해 사용한다.

ex) /root/src/moduleA.ts에서 import {b} from './moduleB'를 한다면

  1. /root/src/moduleB.ts
  2. /root/src/moduleB.tsx
  3. /root/src/moduleB.d.ts
  4. /root/src/moduleB/package.json (types라는 속성이 존재한다면)
  5. /root/src/moduleB/index.ts
  6. /root/src/moduleB/index.tsx
  7. /root/src/moduleB/index.d.ts

ex) /root/src/moduleA.ts에서 import {b} from 'moduleB'을 한다면

  1. /root/src/node_modules/moduleB.ts

  2. /root/src/node_modules/moduleB.tsx

  3. /root/src/node_modules/moduleB.d.ts

  4. /root/src/node_modules/moduleB/package.json (if it specifies a "types" property)

  5. /root/src/node_modules/@types/moduleB.d.ts

  6. /root/src/node_modules/moduleB/index.ts

  7. /root/src/node_modules/moduleB/index.tsx

  8. /root/src/node_modules/moduleB/index.d.ts

  9. /root/node_modules/moduleB.ts

  10. /root/node_modules/moduleB.tsx

  11. /root/node_modules/moduleB.d.ts

  12. /root/node_modules/moduleB/package.json (if it specifies a "types" 1. property)

  13. /root/node_modules/@types/moduleB.d.ts

  14. /root/node_modules/moduleB/index.ts

  15. /root/node_modules/moduleB/index.tsx

  16. /root/node_modules/moduleB/index.d.ts

  17. /node_modules/moduleB.ts

  18. /node_modules/moduleB.tsx

  19. /node_modules/moduleB.d.ts

  20. /node_modules/moduleB/package.json (if it specifies a "types" property)

  21. /node_modules/@types/moduleB.d.ts

  22. /node_modules/moduleB/index.ts

  23. /node_modules/moduleB/index.tsx

  24. /node_modules/moduleB/index.d.ts

추가적인 모듈 해석 플래그

프로젝트 소스들이 결과물과 매치가 되지 않는 경우가 종종 발생합니다.

최종 아웃풋을 생성안에서 빌드 스텝들이 발생한다.

그 스텝들은 ts 파일을 js 파일로 컴파일하는 것과 다른 소스 로케이션에서 하나의 아웃풋 로케이션로 디펜던시를 복사하는 것을 포함한다.

런타임때 모듈은 정의 파일을 포함한 소스 파일에서 다른 이름을 가질 수 있습니다.

또는 최종 결과물의 모듈 경로가 컴파일시 소스 파일 경로와 일치하지 않을 수 있습니다.

타입스크립트 컴파일러는 최종 결과물을 생성하기 위해 소스에서 발생될 변환에 대한 정보를 제공하기 위한 추가적인 플래그가 있습니다.

컴파일러는 이러한 변환을 하지 않습니다. 다만 정의 파일을 불러올때 모듈을 해석하는 과정을 안내하는데 사용됩니다.

Base URL

baseUrl을 사용하는 것은 런타임에 단일 폴더로 배포가 되는 AMD 모듈을 사용하는 애플리케이션에서는 흔한 방법입니다.

모듈의 소스들은 다른 디렉토리에서 존재할수 있으나 빌드 스크립트는 소스들을 하나로 모아둡니다.

baseUrl을 설정하는 것은 컴파일러에게 모듈을 어디서 찾는지 알려주는 것입니다.

상대적이지 않은 모듈 불러오기는 baseUrl에 상대적인 것으로 가정합니다.

상대적인 모듈 불러오기는 baseUrl을 세팅하는 것에 영향을 받지 않습니다.

상대적인 모듈 불러오기는 항상 불러오는 파일에서 상대적으로 해석이 됩니다.

경로 맵핑

가끔 모듈이 baseUrl 아래에 직접 위치하지 않습니다.

예를 들어 jquery 모듈을 불러올떄 런타임때 "node_modules/jquery/dist/jquery.slim.min.js"로 변경될 수 있ㅅ븐디ㅏ.

로더는 런타임때 모듈이름을 맵핑하기 위해서 맵핑 설정을 사용합니다.

타입스크립트 컴파일러는 tsconfig.json 안에 있는 paths 속성을 사용해서 선언을 지원해줍니다.

{
"compilerOptions": {
"baseUrl": ".", // This must be specified if "paths" is.
"paths": {
"jquery": ["node_modules/jquery/dist/jquery"] // This mapping is relative to "baseUrl"
}
}
}

'paths'는 'baseUrl'에 상대적으로 해석된다는 것을 유념해야합니다.

만약 baseUrl이 '.'이 아닌 다른 값으로 설정했따면 맵핑 또한 동일하게 바꿔야 합니다.

'baseUrl: "./src"'라고 했다면 jqeury 또한 "../node_modules/..."로 변경해야합니다.

pahts는 또한 메우 복잡한 다수의 폴백 위치를 포함하는 맵핑을 허용합니다.

한 위치에서만 일부 모듈을 사용할수 있도 나머지는 다른 위치에 있는 프로젝트 구성을 해도 빌드 단계에서는 흩어져 있는 것을 하나로 묶을 수 있습니다.

projectRoot
├── folder1
│ ├── file1.ts (imports 'folder1/file2' and 'folder2/file3')
│ └── file2.ts
├── generated
│ ├── folder1
│ └── folder2
│ └── file3.ts
└── tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"*": [
"*",
"generated/*"
]
}
}
}

"*" 패턴과 일치하는 모듈 불러오기는 컴파일러에게 2가지 위치에서 찾게 알려줍니다.

  1. "*": 변경되지 않은 같은 이름을 의미하기 떄문에 moduleName을 baseUrl/moduleName으로 매핑합니다.
  2. 'genrated/*'는 genereated라는 접두사가 붙은 모듈이름을 의미합니다. 그러므로 moduleName을 baseUrl/generated/moduleName으로 매핑합니다.

컴파일러는 2가지 불러오기로 해석을 시도합니다.

ex) import 'folder/file2'

  1. "*" 패턴과 일치하고 와일드카드는 모듈이름을 캡쳐합니다.
  2. 리스트에서 처음으로 치환을 합니다: "*" -> 'folder/file2'
  3. baseUrl과 위 결과물을 결합해서 상대적이지 않은 이름으로 치환합니다. 'folder.file2' -> '[baseUrl]/folder/file2.ts'
  4. 파일이 존재한다면 끝납니다.

ex) import 'folder/file3'

  1. "*" 패턴과 일치하고 와일드카드는 모듈이름을 캡쳐합니다.
  2. 리스트에서 처음으로 치환을 합니다: "*" -> 'folder/file2'
  3. baseUrl과 위 결과물을 결합해서 상대적이지 않은 이름으로 치환합니다. 'folder.file2' -> '[baseUrl]/folder/file2.ts'
  4. 파일이 존재하지 않으면 다음 치환으로 넘어갑니다.
  5. 두 번쨰 치환을 합니다 'generated/*' -> 'generated/folder/file3'
  6. baseUrl과 위 결과물을 결합해서 상대적이지 않은 이름으로 치환합니다. '[baseUrl]/genreated/folder/file3.ts'
  7. 만약 존재한다면 끝납니다.

링크

typescript-module-resolution typescript-gitbook