모듈 해석 전략
node·bundler·classic 해석 전략이 import 경로를 실제 파일과 타입 선언으로 찾는 순서를 이해하고 추적 옵션으로 진단합니다.
타입스크립트 컴파일러(tsc)는 import 또는 export 문에서 참조하는 모듈의 실제 파일을 찾아내는 복잡한 과정을 거칩니다.
이 과정을 모듈 해석(Module Resolution)이라고 하며, 타입스크립트가 소스 코드의 타입을 정확하게 확인하고 올바른 자바스크립트 코드를 생성하는 데 매우 중요합니다.
타입스크립트는 Node.js 환경의 모듈 해석 방식과 유사하게 동작하며, tsconfig.json 파일의 moduleResolution 컴파일러 옵션을 통해 이 전략을 설정할 수 있습니다.
모듈 해석의 기본 원리
타입스크립트 컴파일러는 다음 두 가지 유형의 모듈 참조를 처리해야 합니다.
상대 참조 (./, ../):
import { someFunc } from './myFile'; 또는 import { anotherFunc } from '../components/AnotherComponent';
이러한 참조는 현재 파일의 위치를 기준으로 상대 경로를 사용하여 다른 파일을 찾습니다.
비-상대 참조 (Non-relative imports):
import * as React from 'react'; 또는 import { format } from 'date-fns';
이러한 참조는 절대 경로, baseUrl, paths 매핑 또는 node_modules 폴더 내에서 모듈을 찾습니다.
모듈 해석의 핵심은 다음과 같습니다.
- 파일 찾기:
import문에 지정된 이름에 해당하는 파일을 찾아야 합니다..ts,.tsx,.d.ts,.js,.jsx등 다양한 확장자를 고려합니다. - 타입 정의 파일 찾기: 실제
.js파일 외에도 해당 모듈의 타입 정의 파일(.d.ts)을 찾아야 정확한 타입 정보를 얻을 수 있습니다.
moduleResolution 컴파일러 옵션
모듈 해석이 의존하는 표면 — 상태 추적
독립 폴더 실습에서 paths alias는 검사 통과 뒤 출력 문자열이 남아 Node에서 실행이 실패하고, 교정한 상대 .js 경로는 14를 출력한다. 구성/산출/실행 결과의 같은 역할로 두 실습을 비교할 가치가 있다.
TYPE / RUNTIME / CONTRACT
타입 파일을 찾는 규칙과 실행 파일을 찾는 규칙을 맞춘다
TypeScript는 import 문자열을 해석해 타입을 검사합니다. 출력 JavaScript의 같은 경로를 Node나 번들러가 찾는지는 별도로 확인해야 합니다.
위는 실행 별칭이 없는 독립 폴더에서 paths만 지정한 실습이고, 아래는 NodeNext와 상대 .js 경로로 교정한 실습입니다. 두 영역은 독립적이며 컴파일러가 파일을 찾았다는 사실만으로 Node가 같은 문자열을 찾을 수 있다고 결론내리지 않습니다.
NODE DIRECT
NodeNext로 Node 규칙을 모델링
Node 직접 실행은 package type, 파일 확장자, import/require 조건을 함께 확인합니다. node(node10)는 구형 CommonJS 해석이며 현재 Node ESM의 규칙을 대신하지 않습니다.
BUNDLER
실제 번들러의 규칙에 맞춘다
bundler 모드는 package exports/imports와 상대 경로 확장자 생략을 지원합니다. Node가 출력 ESM을 직접 로드하는 구성과는 다릅니다.
PATHS
별칭은 출력 import를 바꾸지 않는다
본문의 @utils는 타입 검사에서 src/utils/index.ts에 대응합니다. 출력에도 @utils가 남으므로 번들러나 실행 환경의 별칭 지원이 필요합니다.
PACKAGE METADATA
공개 조건과 타입 대상을 찾는다
패키지 이름은 exports, types와 해당 해석 모드에 따라 찾습니다. relative 경로와 package 이름을 동일한 고정 탐색 순서로 읽지 마세요.
GLOBAL TYPES
types와 typeRoots의 목적
types는 자동 전역 포함 패키지를 제한합니다. 목록 밖 패키지라도 명시적으로 import한 모듈의 타입 해석을 막지는 않습니다. typeRoots도 패키지 타입 탐색 범위를 지정합니다.
TRACE / RUN
타입 해석과 실행을 각각 확인
tsc --traceResolution으로 컴파일러가 실제 선택한 후보를 봅니다. 타입 검사 성공 이후에도 출력 경로와 파일 형식을 실제 실행 환경에서 확인합니다.
rootDirs는 상대 경로 타입 검사를 위한 가상 루트입니다. 출력 파일을 합치거나 실행 디렉토리를 바꾸지는 않습니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 관계·조건 설명 | 위는 실행 별칭이 없는 독립 폴더에서 paths만 지정한 실습이고, 아래는 NodeNext와 상대 .js 경로로 교정한 실습입니다. 두 영역은 독립적이며 컴파일러가 파일을 찾았다는 사실만으로 Node가 같은 문자열을 찾을 수 있다고 결론 내리지 않습니다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| alias 검사 | 타입 해석: 성공 / JS specifier: @utils/math | paths로 @utils/math를 src 파일로 해석한다 | paths는 출력 문자열을 다시 쓰지 않는다 |
| alias 실행 실패 | Node: 해석 실패 / 타입 검사: 성공과 별개 | 별칭 loader가 없는 독립 Node 실행은 패키지를 찾지 못한다 | 컴파일러의 파일 발견과 runtime 로딩을 구분한다 |
| 상대 .js 경로 교정 | 출력: ./utils/math.js / Node 결과: 14 | 다른 독립 실습은 ./utils/math.js를 출력하고 14를 얻는다 | 두 구성을 같은 runtime에서 연속으로 자동 수정한 것으로 읽지 않는다 |
모듈 해석이 의존하는 표면 — 상태 추적
독립 폴더 실습에서 paths alias는 검사 통과 뒤 출력 문자열이 남아 Node에서 실행이 실패하고, 교정한 상대 .js 경로는 14를 출력한다. 구성/산출/실행 결과의 같은 역할로 두 실습을 비교할 가치가 있다.
TYPE / RUNTIME / CONTRACT
타입 파일을 찾는 규칙과 실행 파일을 찾는 규칙을 맞춘다
TypeScript는 import 문자열을 해석해 타입을 검사합니다. 출력 JavaScript의 같은 경로를 Node나 번들러가 찾는지는 별도로 확인해야 합니다.
위는 실행 별칭이 없는 독립 폴더에서 paths만 지정한 실습이고, 아래는 NodeNext와 상대 .js 경로로 교정한 실습입니다. 두 영역은 독립적이며 컴파일러가 파일을 찾았다는 사실만으로 Node가 같은 문자열을 찾을 수 있다고 결론내리지 않습니다.
NODE DIRECT
NodeNext로 Node 규칙을 모델링
Node 직접 실행은 package type, 파일 확장자, import/require 조건을 함께 확인합니다. node(node10)는 구형 CommonJS 해석이며 현재 Node ESM의 규칙을 대신하지 않습니다.
BUNDLER
실제 번들러의 규칙에 맞춘다
bundler 모드는 package exports/imports와 상대 경로 확장자 생략을 지원합니다. Node가 출력 ESM을 직접 로드하는 구성과는 다릅니다.
PATHS
별칭은 출력 import를 바꾸지 않는다
본문의 @utils는 타입 검사에서 src/utils/index.ts에 대응합니다. 출력에도 @utils가 남으므로 번들러나 실행 환경의 별칭 지원이 필요합니다.
PACKAGE METADATA
공개 조건과 타입 대상을 찾는다
패키지 이름은 exports, types와 해당 해석 모드에 따라 찾습니다. relative 경로와 package 이름을 동일한 고정 탐색 순서로 읽지 마세요.
GLOBAL TYPES
types와 typeRoots의 목적
types는 자동 전역 포함 패키지를 제한합니다. 목록 밖 패키지라도 명시적으로 import한 모듈의 타입 해석을 막지는 않습니다. typeRoots도 패키지 타입 탐색 범위를 지정합니다.
TRACE / RUN
타입 해석과 실행을 각각 확인
tsc --traceResolution으로 컴파일러가 실제 선택한 후보를 봅니다. 타입 검사 성공 이후에도 출력 경로와 파일 형식을 실제 실행 환경에서 확인합니다.
rootDirs는 상대 경로 타입 검사를 위한 가상 루트입니다. 출력 파일을 합치거나 실행 디렉토리를 바꾸지는 않습니다.
이 예의 조건
- 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
- 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
| 구분 | 내용 |
|---|---|
| 전체 관계·조건 설명 | 위는 실행 별칭이 없는 독립 폴더에서 paths만 지정한 실습이고, 아래는 NodeNext와 상대 .js 경로로 교정한 실습입니다. 두 영역은 독립적이며 컴파일러가 파일을 찾았다는 사실만으로 Node가 같은 문자열을 찾을 수 있다고 결론 내리지 않습니다. |
| 장면 | 현재 관찰값 | 설명 | 변화 이유 |
|---|---|---|---|
| alias 검사 | 타입 해석: 성공 / JS specifier: @utils/math | paths로 @utils/math를 src 파일로 해석한다 | paths는 출력 문자열을 다시 쓰지 않는다 |
| alias 실행 실패 | Node: 해석 실패 / 타입 검사: 성공과 별개 | 별칭 loader가 없는 독립 Node 실행은 패키지를 찾지 못한다 | 컴파일러의 파일 발견과 runtime 로딩을 구분한다 |
| 상대 .js 경로 교정 | 출력: ./utils/math.js / Node 결과: 14 | 다른 독립 실습은 ./utils/math.js를 출력하고 14를 얻는다 | 두 구성을 같은 runtime에서 연속으로 자동 수정한 것으로 읽지 않는다 |
tsconfig.json의 compilerOptions.moduleResolution 옵션은 타입스크립트가 모듈을 해석하는 방식을 결정합니다.
아래 전략은 서로 다른 실행 환경과 세대를 대상으로 합니다.
컴파일러 버전도 먼저 확인하기
이 절의 node10, classic, baseUrl 예제는 구형 설정을 읽기 위한 설명입니다. TypeScript 6.0에서는 이 옵션들에 폐기 예정 진단이 납니다. 설치 버전은 tsc --version으로 확인하고, 새 직접 실행 예제는 NodeNext, 실제 번들러를 사용하는 예제는 그 환경의 해석 모드를 선택합니다. 아래 실습은 baseUrl 없이 명시적인 paths를 사용합니다. 구형 설정을 유지하기 위한 임시 진단 억제와 새 설정으로의 전환은 다른 결정입니다.
"node" (node10)와 "NodeNext": node는 구형 CommonJS 해석을 모델링합니다. 현재 Node.js에서 직접 실행하는 코드는 NodeNext 등 Node 전용 모드와 일치하는 module 설정을 사용해 ESM·CommonJS와 패키지 조건을 구분합니다.
"bundler" (TypeScript 5.0+): 모던 번들러(Webpack, Vite, Rollup 등)의 해석 방식을 모방합니다.
웹 개발(프론트엔드) 프로젝트에서 권장됩니다.
"classic" (Legacy): 타입스크립트 초기에 사용되던 구식 전략입니다.
현재는 거의 사용되지 않습니다.
각 전략의 주요 특징을 살펴보겠습니다.
"node" 전략
node 전략은 구형 CommonJS 탐색에 TypeScript의 소스·선언 파일 후보를 더합니다. 아래 목록은 주요 후보의 개략이며 .jsx, 패키지 메타데이터, 타입 탐색 단계까지 포함한 완전한 탐색 순서는 아닙니다.
-
상대 경로:
import { x } from './moduleA'는 다음과 같은 순서로 파일을 찾습니다../moduleA.ts./moduleA.tsx./moduleA.d.ts./moduleA.js(실제 JavaScript 파일)./moduleA/index.ts./moduleA/index.tsx./moduleA/index.d.ts./moduleA/index.js -
비-상대 경로:
import { x } from 'moduleB'는 다음과 같은 순서로 파일을 찾습니다.현재 디렉터리부터 시작하여 상위 디렉터리로 이동하면서 각
node_modules폴더를 탐색합니다../node_modules/moduleB./node_modules/moduleB/package.json에서main또는types필드를 확인하여 진입점을 찾습니다.main이"./dist/index.js"라면 패키지 루트 기준 경로로 해석하며, TypeScript는 대응하는 타입 파일도 탐색합니다../node_modules/moduleB.ts,./node_modules/moduleB.d.ts등도 탐색합니다.
예시: 프로젝트 구조
src/app.ts에서
import { helperFunction } from './utils/helper'; // 상대 경로
import { someValue } from 'some-package'; // 비-상대 경로 (node_modules에서 찾음)
helperFunction();
console.log(someValue);이 예제는 구형 node 탐색을 설명합니다. 실제 Node.js ESM과 현대 번들러에는 각각의 전용 모드가 필요합니다.
"bundler" 전략 (TypeScript 5.0+)
bundler 전략은 Webpack, Vite, Rollup 등 현대적인 자바스크립트 번들러가 모듈을 해석하는 방식과 더 가깝습니다.
이 전략은 exports·imports 조건을 지원하면서 상대 경로의 확장자 생략 같은 번들러 관행을 허용합니다. Node.js에서 직접 실행할 ESM의 검사를 대신하지는 않습니다.
"exports"필드 지원:package.json의"exports"필드를 사용하여 조건부 내보내기(conditional exports)를 더 정확하게 해석합니다. 이는 Node.js의 새로운 모듈 시스템과 웹 번들러의 동작을 통일하는 데 중요합니다.- import·require 조건: 사용한 구문과 모듈 설정에 맞는 패키지 조건을 선택합니다. 실제 출력의 ESM·CommonJS 판정은 실행 주체의 규칙도 확인합니다.
- Node.js의
main필드 대신exports필드를 우선적으로 고려합니다.
bundler는 실제로 번들러가 출력과 의존성을 처리하는 프로젝트에서 선택합니다. Node.js가 출력 파일을 직접 로드하면 Node 전용 해석 모드를 선택합니다.
"classic" 전략 (권장되지 않음)
classic 전략은 타입스크립트 초기에 존재하던 방식입니다.
상대 경로가 아닌 모듈을 찾을 때 node_modules를 탐색하지 않고, 단순히 현재 디렉터리와 상위 디렉터리에서 .ts, .d.ts 파일을 찾는 방식으로 동작합니다.
초기 TypeScript의 AMD 등 모듈 구성을 위해 쓰이던 방식이며, 일반 script 태그의 런타임 파일 로딩 규칙을 제공하지는 않습니다.
현재는 거의 사용되지 않습니다.
추가적인 모듈 해석 관련 tsconfig.json 옵션
moduleResolution 외에도 모듈 해석에 영향을 미치는 중요한 compilerOptions가 있습니다.
-
baseUrl: 비-상대 모듈 임포트의 기준이 되는 디렉터리를 지정합니다.이 옵션이 설정되면, 타입스크립트는
node_modules를 검색하기 전에baseUrl을 기준으로 모듈을 찾으려고 시도합니다.tsconfig.json // tsconfig.json { "compilerOptions": { "baseUrl": "./src" } }// src/app.ts import { MyComponent } from 'components/MyComponent'; // ./src/components/MyComponent를 찾음 -
paths: 모듈 경로를 컴파일러가 찾을 위치에 매핑합니다.baseUrl없이도 사용할 수 있으며, 출력 import를 다시 쓰거나 런타임 별칭을 생성하지 않습니다.복잡한 상대 경로를 줄이고 모듈 구조를 추상화하는 데 매우 유용합니다.
tsconfig.json // tsconfig.json { "compilerOptions": { "baseUrl": ".", "paths": { "@app/*": ["src/app/*"], "@utils": ["src/utils/index.ts"] } } }src/app/pages/home.ts import { fetchData } from '@utils'; // src/utils/index.ts import { User } from '@app/models/user'; // src/app/models/user.ts -
rootDirs: 여러 디렉터리가 합쳐질 것을 가정해 상대 import를 타입 검사할 가상 루트 목록입니다. 출력이나 런타임 디렉터리 구조를 직접 바꾸지는 않습니다.이는 빌드 시스템(번들러)이 여러 소스 디렉터리를 하나의 가상 디렉터리로 병합하는 경우에 유용합니다.
-
typeRoots: 전역에 자동 포함할 타입 패키지를 지정한 디렉터리들로 제한합니다. 설정하면 기본으로 보이던node_modules/@types도 필요한 경우 목록에 직접 넣습니다. -
types: 전역 타입 선언을 포함할 특정@types패키지 목록을 지정합니다.이 목록에 없는
@types패키지는 전역에 자동 포함되지 않습니다. 명시적으로 import한 모듈의 타입 해석을 막는 옵션은 아닙니다.
모듈 해석 과정 디버깅
모듈 해석이 예상대로 동작하지 않을 때는 tsc 명령어에 --traceResolution 플래그를 추가하여 자세한 해석 과정을 확인할 수 있습니다.
tsc --traceResolution이 명령어는 컴파일러가 어떤 파일을 어디서 찾으려 했는지, 어떤 규칙을 적용했는지 등 상세한 로그를 출력하여 문제를 진단하는 데 큰 도움을 줍니다.
타입 검사 성공 뒤 실행 경로를 확인하는 실습
별칭을 처리하는 실제 번들러가 없는 독립 폴더에서 진행합니다. package.json은 { "type": "module" }로 저장합니다. 아래 세 파일을 만들고, 먼저 컴파일러가 어떤 파일을 찾을지 예상해 보세요.
export function double(value: number): number {
return value * 2;
}import { double } from '@utils/math';
console.log(double(7));{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"rootDir": "src",
"outDir": "dist",
"strict": true,
"types": [],
"paths": { "@utils/*": ["./src/utils/*"] }
},
"include": ["src/**/*.ts"]
}tsc -p . --traceResolution
node dist/app.js추적 로그에서 @utils/math를 src/utils/math.ts로 해석한 부분을 찾습니다. 컴파일은 성공하지만 출력 dist/app.js의 가져오기 경로는 계속 @utils/math입니다. 이 폴더에는 해당 패키지나 실행 별칭이 없으므로 Node 직접 실행은 모듈을 찾지 못해 실패합니다. 14를 출력하지 않습니다. bundler를 선택했다는 사실만으로 실제 번들러가 설치되거나 실행된 것도 아닙니다.
이 실습을 Node 직접 실행으로 바꾸려면 두 가지를 함께 맞춥니다. tsconfig.json의 module과 moduleResolution을 모두 NodeNext로 바꾸고, 위 app.ts의 가져오기 경로를 ./utils/math.js로 바꿉니다. 다시 tsc -p ., node dist/app.js를 실행하면 14입니다. 컴파일러는 대응하는 .ts 소스를 검사하고 Node는 생성된 .js를 읽습니다.
다른 파일 이름으로 적용하기
직접 실행 구성을 유지한 상태에서 math.ts를 helper.ts로 옮기고 호출 값을 9로 바꿨다고 가정합니다. 새 가져오기 문자열과 출력은 무엇일까요? 파일 이름만 옮기고 가져오기 문자열을 그대로 두면 어떤 단계에서 문제가 드러날지도 적어보세요. 오래된 출력이 섞이지 않도록 이 변형은 새 폴더에서 실행합니다.
입력 소스와 실행 파일 이름 확인
새 문자열은 ./utils/helper.js, 출력은 18입니다. 소스 파일 이름이 바뀌었으므로 이전 ./utils/math.js에 대응하는 소스를 찾지 못하는 타입 해석 진단이 납니다. 경로를 수정하고 다시 컴파일하면 새 실행 파일을 찾을 수 있습니다. 이 문제는 인자 타입이나 계산식이 틀린 경우와 구별합니다.
paths가 출력을 바꾸지 않는 이유는 공식 모듈 참조, 구형 옵션의 전환은 TypeScript 6.0 설명에서 대조할 수 있습니다.
모듈 해석 전략은 타입스크립트 프로젝트의 빌드 설정과 직접적으로 연관되어 있습니다.
프로젝트 환경(Node.js, 브라우저, 번들러 사용 여부 등)에 맞춰 moduleResolution 옵션을 올바르게 설정하는 것이 중요합니다.
Node.js 직접 실행은 Node 전용 모드, 번들러 실행은 bundler 모드를 기준으로 검토합니다.
baseUrl, paths 같은 추가 옵션을 활용하면 모듈 임포트 경로를 더 깔끔하게 관리하고 개발 경험을 개선할 수 있습니다.
모듈을 찾지 못하는 오류가 생기면 import 문자열부터 패키지의 exports와 types 필드까지 순서대로 확인하는 것이 좋습니다.
해석 실패를 줄이려면 설정 변경 전후로 실제 후보 파일, 패키지 메타데이터, 번들러 규칙이 같은 방향을 가리키는지 함께 봐야 합니다.
모듈 해석 전략을 코드에 적용하기 전, 컴파일 오류가 막아 줄 지점과 사람이 약속해야 할 지점을 나눕니다.