모듈 해석(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
1-1. 상대적(Relative) import
/
, ./
, 또는 ../
으로 시작한다. import
코드가 작성된 파일의 경로를 기준으로 상대적으로 해석되며, 앰비언트 모듈 선언은 활용되지 않는다. 직접 만든 모듈 중에 런타임 시 상대적 위치가 유지되는 것이 보장되는 모듈을 import
하고 싶다면 상대적 import
를 사용하면 된다.import Entry from "./components/Entry";
import { DefaultHeaders } from "../constants/http";
import "/mod";
1-2. 비-상대적(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가 채택했던 기본 전략이다. 다만 오늘날에는 거의 사용되지 않고, 오로지 하위 호환성을 보장하기 위한 목적으로 존재하고 있다.
2-1-1. 상대적 import
EX)
/root/src/folder/A.ts
파일에 작성된 import { b } from "./moduleB"
코드/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
2-1-2. 비-상대적 import
EX)
/root/src/folder/A.ts
파일에 작성된 import { b } from "moduleB"
코드/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
/root/src/moduleB.ts
/root/src/moduleB.d.ts
/root/moduleB.ts
/root/moduleB.d.ts
/moduleB.ts
/moduleB.d.ts
2-2. Node
Node.js에서
require()
함수를 이용한 import
를 해석하는 과정을 모방한 전략이다. 이것도 마찬가지로 상대적 import
와 비-상대적 import
의 모듈 해석 과정이 다르다. 여기서는 TypeScript가 그 전략을 어떻게 모방했는지만 설명할 것이다. 어차피 Node.js의 모듈 해석 전략이랑 거의 동일하기 때문이다. Node.js의 모듈 해석 전략이 궁금하다면 여기를 참조하자.2-2-1. 상대적 import
EX)
/root/src/moduleA.ts
파일에 작성된 import { b } from "./moduleB"
코드/root/src/moduleB.ts
/root/src/moduleB.tsx
/root/src/moduleB.d.ts
/root/src/moduleB/package.json (types 프로퍼티 참조)
/root/src/moduleB/index.ts
/root/src/moduleB/index.tsx
/root/src/moduleB/index.d.ts
2-2-2. 비-상대적 import
EX)
/root/src/moduleA.ts
파일에 작성된 import { b } from "moduleB"
코드baseUrl
프로퍼티 또는paths
프로퍼티를 기준으로 한 상대적 해석 (아래에서 자세히 다룬다.)
/root/src/node_modules/moduleB.ts
/root/src/node_modules/moduleB.tsx
/root/src/node_modules/moduleB.d.ts
/root/src/node_modules/moduleB/package.json
(types
프로퍼티 참조)
/root/src/node_modules/moduleB/index.ts
/root/src/node_modules/moduleB/index.tsx
/root/src/node_modules/moduleB/index.d.ts
/root/src/node_modules/@types/moduleB.d.ts
/root/src/node_modules/@types/moduleB/package.json
(types
프로퍼티 참조)
/root/src/node_modules/@types/moduleB/index.d.ts
/root/node_modules/moduleB.ts
/root/node_modules/moduleB.tsx
/root/node_modules/moduleB.d.ts
/root/node_modules/moduleB/package.json
(types
프로퍼티 참조)
/root/node_modules/moduleB/index.ts
/root/node_modules/moduleB/index.tsx
/root/node_modules/moduleB/index.d.ts
/root/node_modules/@types/moduleB.d.ts
/root/node_modules/@types/moduleB/package.json
(types
프로퍼티 참조)
/root/node_modules/@types/moduleB/index.d.ts
/node_modules/moduleB.ts
/node_modules/moduleB.tsx
/node_modules/moduleB.d.ts
/node_modules/moduleB/package.json
(types
프로퍼티 참조)
/node_modules/moduleB/index.ts
/node_modules/moduleB/index.tsx
/node_modules/moduleB/index.d.ts
/node_modules/@types/moduleB.d.ts
/node_modules/@types/moduleB/package.json
(types
프로퍼티 참조)
/node_modules/@types/moduleB/index.d.ts
- 컴파일 목록에 포함된 앰비언트 모듈 선언 파일들(
.d.ts
files withdeclare module
) 참조
3. Additional module resolution flags
3-1. baseUrl 프로퍼티
이 프로퍼티가 지정되어 있다면 비-상대적
import
의 모듈 해석 과정에 하나의 과정을 추가한다. 바로 위 섹션에서 비-상대적 import
의 모듈 해석 과정 중 1번을 제대로 설명하지 않고 넘어간 것은 여기서 설명하기 위함이었다. baseUrl
프로퍼티가 지정되어 있지 않다면 1번 과정은 생략이 된다. 그러나 만약 baseUrl
프로퍼티가 지정되어 있다면 가장 먼저 baseUrl
을 기준으로 상대적 import
의 모듈 해석 과정을 똑같이 진행하게 된다. 물론, 상대적 import
의 모듈 해석 과정에는 아무 영향을 주지 않는다.3-2. 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
- TypeScript 컴파일러가 타입 선언을 참조하는 과정: https://medium.com/naver-fe-platform/타입스크립트-컴파일러가-모듈-타입-선언을-참조하는-과정-5bfc55a88bb6
- 비-상대적
import
해석 시baseUrl
프로퍼티가 지정된 경우: https://stackoverflow.com/questions/64438182/does-typescript-baseurl-flag-override-the-resolution-mechanism-of-moduleresoluti