안동민 개발노트

본문 시작

모듈 해석 전략

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 컴파일러 옵션

모듈 해석이 의존하는 표면

MODULE RESOLUTION · DEPENDENCY GRAPH

모듈 해석이 의존하는 표면

하나의 import specifier는 compiler mode, package metadata, alias와 실제 Node·bundler resolver에 동시에 의존하며 첫 불일치를 추적해야 합니다.

모듈 해석이 의존하는 표면 import specifier가 compiler mode, package exports와 types, paths alias, source 또는 declaration, runtime resolver에 의존하고 parity gate에서 일치를 확인하는 그래프입니다. ENTRY · 0 INimport specifierrelative · package ·aliasCOMPILER · 1 IN해석 모드NodeNext · bundlerPACKAGE · 1 INexports / types공개 조건ALIAS · 1 INpathsemit rewrite 없음RUNTIME · 1 IN실제 resolverNode · bundlerTYPE TARGET · 3 INsource /declarationcompiler가 선택PARITY · 2 IN실행 일치 gatetrace first divergence같은 표면?COMPILER SUCCESS DOES NOT GUARANTEE NODE OR BUNDLER RUNTIME SUCCESS
  1. import specifier

    relative, package, alias 중 어느 후보 공간인지 구분합니다.

  2. compiler 표면

    NodeNext 또는 bundler 모드와 exports/types/paths가 타입 대상을 고릅니다.

  3. runtime 표면

    실제 Node나 bundler resolver가 같은 specifier를 해석합니다.

  4. 일치 gate

    traceResolution으로 첫 분기를 찾고 양쪽 설정을 함께 고칩니다.

paths는 emitted specifier를 바꾸지 않으며 types와 typeRoots는 일반 module import resolution의 대체물이 아닙니다.

tsconfig.json의 compilerOptions.moduleResolution 옵션은 타입스크립트가 모듈을 해석하는 방식을 결정합니다.

아래 전략은 서로 다른 실행 환경과 세대를 대상으로 합니다.

"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 등도 탐색합니다.

예시: 프로젝트 구조

app.ts
helper.ts
index.js
index.d.ts
tsconfig.json

src/app.ts에서

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

이 명령어는 컴파일러가 어떤 파일을 어디서 찾으려 했는지, 어떤 규칙을 적용했는지 등 상세한 로그를 출력하여 문제를 진단하는 데 큰 도움을 줍니다.


모듈 해석 전략은 타입스크립트 프로젝트의 빌드 설정과 직접적으로 연관되어 있습니다.

프로젝트 환경(Node.js, 브라우저, 번들러 사용 여부 등)에 맞춰 moduleResolution 옵션을 올바르게 설정하는 것이 중요합니다.

Node.js 직접 실행은 Node 전용 모드, 번들러 실행은 bundler 모드를 기준으로 검토합니다.

baseUrl, paths 같은 추가 옵션을 활용하면 모듈 임포트 경로를 더 깔끔하게 관리하고 개발 경험을 개선할 수 있습니다.

모듈을 찾지 못하는 오류가 생기면 import 문자열부터 패키지의 exports와 types 필드까지 순서대로 확인하는 것이 좋습니다.

해석 실패를 줄이려면 설정 변경 전후로 실제 후보 파일, 패키지 메타데이터, 번들러 규칙이 같은 방향을 가리키는지 함께 봐야 합니다.


모듈 해석 전략을 코드에 적용하기 전, 컴파일 오류가 막아 줄 지점과 사람이 약속해야 할 지점을 나눕니다.