안녕하세요. IT 엘도라도 에 오신 것을 환영합니다.
글을 쓰는 것은 귀찮지만 다시 찾아보는 것은 더 귀찮습니다.
완전한 나만의 것으로 만들기 위해 지식을 차곡차곡 저장해 보아요.   포스팅 둘러보기 ▼

타입스크립트 (TypeScript)

[TypeScript] 모듈 해석 (Module Resolution)

피그브라더 2020. 12. 30. 13:26
 

Documentation - Module Resolution

How TypeScript resolves modules in JavaScript

www.typescriptlang.org

 

모듈 해석(Module Resolution)이란 컴파일러가 각 import가 어떤 모듈을 가리키는지 해석하는 과정을 의미한다. 예를 들어 import { a } from "moduleA"라는 코드가 있으면, 컴파일러는 a가 올바르게 사용되는지 체크하기 위해 moduleA가 정확히 어떤 모듈을 가리키는지 알아야 한다. 그리고 해당 모듈에 존재하는 a의 타입 정보를 참조해야만 한다. 그 모듈 탐색 과정이 바로 모듈 해석이다.

먼저, 컴파일러는 import 하려는 모듈을 탐색한다. 그 탐색 전략으로는 크게 두 가지가 있다. 하나는 Classic이고, 다른 하나는 Node이다. 이러한 전략들은 컴파일러에게 moduleA에 해당하는 모듈을 어디서 찾아야 하는지 알려주는 역할을 수행한다.

만약 그 탐색 과정이 실패했는데 import 하려는 모듈을 비-상대적(Non-relative)으로 표현했다면, 컴파일러는 앰비언트 모듈 선언(Ambient Module Declaration)을 탐색한다.

이렇게까지 했는데도 컴파일러가 moduleA를 끝내 해석하지 못했다면 컴파일 에러를 발생시킨다. 이 경우 그 에러는 error TS2307: Cannot find module 'moduleA'. 같은 형식일 것이다.

 

1. Relative vs. Non-relative module imports

👉 상대적(Relative) import

/, ./, 또는 ../으로 시작한다. import 코드가 작성된 파일의 경로를 기준으로 상대적으로 해석되며, 앰비언트 모듈 선언은 활용되지 않는다. 직접 만든 모듈 중에 런타임 시 상대적 위치가 유지되는 것이 보장되는 모듈을 import 하고 싶다면 상대적 import를 사용하면 된다.

 

  • import Entry from "./components/Entry";
  • import { DefaultHeaders } from "../constants/http";
  • import "/mod";

 

👉 비-상대적(Non-relative) import

비-상대적 import는 baseUrl을 기준으로 상대적으로 해석될 수 있고, paths 맵핑에 의해서 해석될 수도 있다. 이에 대해서는 아래에서 자세히 다룬다. 또한 앞서 말했듯이 앰비언트 모듈 선언에 의해서 해석될 수도 있다. 외부 의존성 모듈을 import 하고 싶다면 이러한 비-상대적 import를 사용하자.

 

  • import * as $ from "jquery";
  • import { Component } from "@angular/core";

 

2. Module Resolution Strategies

모듈 해석 전략에는 크게 두 가지가 존재한다. 바로 Classic과 Node이다. 모듈 해석 전략은 커맨드 라인에 --moduleResolution 플래그를 사용하여 지정할 수 있다. 특별히 지정하지 않는다면, module 플래그가 CommonJS인 경우에는 Node로, 그렇지 않은 경우에는 Classic으로 자동 지정된다.

가장 많이 사용되고 실제로 대부분의 프로젝트에서 권장되는 모듈 해석 전략은 Node이다. 만약 프로젝트에서 import 혹은 export와 관련한 모듈 해석 문제를 맞닥뜨렸다면, moduleResolution 플래그를 Node로 지정해서 문제가 해결되는지 살펴보도록 하자.


2-1. Classic

과거에 모듈 해석을 위해 TypeScript가 채택했던 기본 전략이다. 다만 오늘날에는 거의 사용되지 않고, 오로지 하위 호환성을 보장하기 위한 목적으로 존재하고 있다.

 

👉 상대적 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

2-2. Node

Node.js에서 require() 함수를 이용한 import를 해석하는 과정을 모방한 전략이다. 이것도 마찬가지로 상대적 import와 비-상대적 import의 모듈 해석 과정이 다르다. 여기서는 TypeScript가 그 전략을 어떻게 모방했는지만 설명할 것이다. 어차피 Node.js의 모듈 해석 전략이랑 거의 동일하기 때문이다. Node.js의 모듈 해석 전략이 궁금하다면 여기를 참조하자.

 

👉 상대적 import

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

 

👉 비-상대적 import

EX) /root/src/moduleA.ts 파일에 작성된 import { b } from "moduleB" 코드

 

  1. baseUrl 프로퍼티 또는 paths 프로퍼티를 기준으로 한 상대적 해석 (아래에서 자세히 다룬다.)
  2. /root/src/node_modules/moduleB.ts
  3. /root/src/node_modules/moduleB.tsx
  4. /root/src/node_modules/moduleB.d.ts
  5. /root/src/node_modules/moduleB/package.json (types 프로퍼티 참조)
  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/src/node_modules/@types/moduleB.d.ts
  10. /root/src/node_modules/@types/moduleB/package.json (types 프로퍼티 참조)
  11. /root/src/node_modules/@types/moduleB/index.d.ts
  12. /root/node_modules/moduleB.ts
  13. /root/node_modules/moduleB.tsx
  14. /root/node_modules/moduleB.d.ts
  15. /root/node_modules/moduleB/package.json (types 프로퍼티 참조)
  16. /root/node_modules/moduleB/index.ts
  17. /root/node_modules/moduleB/index.tsx
  18. /root/node_modules/moduleB/index.d.ts
  19. /root/node_modules/@types/moduleB.d.ts
  20. /root/node_modules/@types/moduleB/package.json (types 프로퍼티 참조)
  21. /root/node_modules/@types/moduleB/index.d.ts
  22. /node_modules/moduleB.ts
  23. /node_modules/moduleB.tsx
  24. /node_modules/moduleB.d.ts
  25. /node_modules/moduleB/package.json (types 프로퍼티 참조)
  26. /node_modules/moduleB/index.ts
  27. /node_modules/moduleB/index.tsx
  28. /node_modules/moduleB/index.d.ts
  29. /node_modules/@types/moduleB.d.ts
  30. /node_modules/@types/moduleB/package.json (types 프로퍼티 참조)
  31. /node_modules/@types/moduleB/index.d.ts
  32. 컴파일 목록에 포함된 앰비언트 모듈 선언 파일들(.d.ts files with declare module) 참조

 

3. Additional module resolution flags

👉 baseUrl 프로퍼티

이 프로퍼티가 지정되어 있다면 비-상대적 import의 모듈 해석 과정에 하나의 과정을 추가한다. 바로 위 섹션에서 비-상대적 import의 모듈 해석 과정 중 1번을 제대로 설명하지 않고 넘어간 것은 여기서 설명하기 위함이었다. baseUrl 프로퍼티가 지정되어 있지 않다면 1번 과정은 생략이 된다. 그러나 만약 baseUrl 프로퍼티가 지정되어 있다면 가장 먼저 baseUrl을 기준으로 상대적 import의 모듈 해석 과정을 똑같이 진행하게 된다. 물론, 상대적 import의 모듈 해석 과정에는 아무 영향을 주지 않는다.


👉 paths 프로퍼티

baseUrl 프로퍼티가 지정되어 있는 경우에만 유효한 프로퍼티이다. 만약 baseUrl뿐 아니라 paths 프로퍼티까지 지정되어 있다면, 바로 위에서 설명한 baseUrl에 근거한 모듈 해석 과정을 진행하지 않고 paths 프로퍼티에 지정된 패턴들을 활용한 모듈 해석 과정을 대신 진행한다. 구체적으로는, paths 프로퍼티에 지정된 패턴들 중 import 하려는 모듈의 이름과 매칭되는 것을 찾고, 발견된다면 그 패턴에 맵핑된 경로(baseUrl을 기준으로 한 상대 경로)를 기준으로 상대적 import의 모듈 해석 과정을 똑같이 진행하게 된다. 물론, 이 역시도 상대적 import의 모듈 해석 과정에는 아무 영향을 주지 않는다.

즉, baseUrl 프로퍼티가 지정되어 있거나 baseUrl 프로퍼티와 더불어 paths 프로퍼티까지 함께 지정되어 있으면 비-상대적 import의 모듈 해석 과정의 맨 앞에 한 단계가 더 추가되는 것이다. 만약 여기서 탐색에 실패하면 다시 원칙대로 모듈 해석 과정을 진행하게 된다.

 

4. Tracing module resolution

앞서 말했듯, 컴파일러는 비-상대적 import의 모듈 해석 시에 현재 경로에서 시작하여 상위로 올라가며 탐색한다. 그런데 이러한 방식은 어떠한 모듈이 왜 해석되지 않는 것인지, 혹은 어떠한 모듈이 왜 잘못 해석이 되는지 등을 파악하고자 할 때 혼란을 줄 수 있다. 이때 그 모듈 해석 과정을 추적해볼 수 있도록 하는 플래그가 바로 --traceResolution이다. 이 플래그를 지정하여 tsc를 실행하면 모듈 해석 과정을 처음부터 끝까지 살펴볼 수 있다.

 

5. Using --noResolve

기본적으로 컴파일러는 본격적인 컴파일을 시작하기 전에 모든 import들을 해석한다. 그리고 성공적으로 하나의 import를 해석할 때마다 그 타겟 파일은 추후 컴파일러가 처리하게 될 파일들의 목록(= 컴파일 목록)에 자동으로 포함이 된다. 이것이 기본 동작이다.

이때 --noResolve 플래그는 컴파일러가 커맨드 라인에 입력되지 않은 파일들은 컴파일 목록에 넣지 않도록 한다. 물론 이 플래그를 지정해줘도 여전히 모듈 해석은 똑같이 진행하지만, 만약 그 타겟 파일이 커맨드 라인에 입력되지 않았다면 이는 컴파일 목록에 포함되지 않는다. 즉, 해석이 성공한 모듈이어도 커맨드 라인에 직접적으로 입력된 파일이 아니라면 컴파일 목록에 넣지 않는 것이다.

 

6. Common Questions

👉 exclude 프로퍼티에 지정한 모듈이 왜 컴파일 목록에 포함될까?

tsconfig.json 파일의 include 프로퍼티는 컴파일 목록에 포함할 파일들의 목록을 지정한다. 그리고 exclude 프로퍼티는 include 프로퍼티에 의해 컴파일 목록에 포함될 파일들 중 제외시킬 파일들의 목록을 지정한다. 그런데 이는 앞서 설명했던 모듈 해석 과정과는 다른 이야기이다. import의 모듈 해석 과정에서 발견된 타겟 파일들은 그때 그때 컴파일 목록에 포함되는 것으로, 이전 단계에서 제외된 파일이어도 문제 없이 컴파일 목록에 포함이 된다. exclude 프로퍼티는 단순히 'include 프로퍼티에 의해' 컴파일 목록에 포함될 파일들 중 제외시킬 파일들을 지정할 뿐이다.

 

따라서 컴파일 목록에서 특정 파일을 완전히 제외하고 싶다면 그 파일뿐만 아니라 그 파일을 가리키는 import 코드 혹은 /// <reference ... /> 디렉티브를 가지고 있는 파일들도 전부 제외시켜줘야 한다. 그렇지 않으면 모듈 해석의 결과로 다시 컴파일 목록에 포함될 것이다.

 

7. Other References