본문으로 건너뛰기

안동민 개발노트

본문 시작

모듈 해석 전략

node·bundler·classic 해석 전략이 import 경로를 실제 파일과 타입 선언으로 찾는 순서를 이해하고 추적 옵션으로 진단합니다.

타입스크립트 컴파일러(tsc)는 import 또는 export 문에서 참조하는 모듈의 실제 파일을 찾아내는 복잡한 과정을 거칩니다.

이 과정을 모듈 해석(Module Resolution)이라고 하며, 타입스크립트가 소스 코드의 타입을 정확하게 확인하고 올바른 자바스크립트 코드를 생성하는 데 매우 중요합니다.

타입스크립트는 Node.js 환경의 모듈 해석 방식과 유사하게 동작하며, tsconfig.json 파일의 moduleResolution 컴파일러 옵션을 통해 이 전략을 설정할 수 있습니다.

모듈 해석 원리와 컴파일러 옵션

타입스크립트 컴파일러(tsc)는 import 또는 export 문에서 참조하는 모듈의 실제 파일을 찾아내는 복잡한 과정을 거칩니다. 이 과정을 모듈 해석(Module Resolution)이라고 하며, 타입스크립트가 소스 코드의 타입을 정확하게 확인하고 올바른 자바스크립트 코드를 생성하는 데 매우 중요합니다.

  1. 1
    모듈 해석의 기본 원리

    타입스크립트 컴파일러는 다음 두 가지 유형의 모듈 참조를 처리해야 합니다. 모듈 해석(Module Resolution)

  2. 2
    moduleResolution 컴파일러 옵션

    tsconfig.json의 compilerOptions.moduleResolution 옵션은 타입스크립트가 모듈을 해석하는 방식을 결정합니다. 상대 참조 (./, ../)

  3. 3
    추가적인 모듈 해석 관련 tsconfig.json 옵션

    moduleResolution 외에도 모듈 해석에 영향을 미치는 중요한 compilerOptions가 있습니다. 비-상대 참조 (Non-relative imports)

  4. 4
    모듈 해석 원리와 컴파일러 옵션 기준

    정확성 이 과정을 모듈 해석(Module Resolution)이라고 하며, 타입스크립트가 import 문자열을 실제 파일과 선언 파일로 연결합니다. 비용 상대 참조(./, ../)는 import { someFunc } from './myFile';처럼 현재 파일 기준으로 경로를 적습니다. 확장성 비-상대 참조(Non-relative imports)는 import * as React from 'react';처럼 패키지 이름이나 별칭을 기준으로 찾습니다. 예외 파일 찾기: import 문에 지정된 이름에 해당하는 파일을 찾아야 합니다.

  5. 5
    정확성

    과정을 모듈 해석(Module Resolution)이라고 하며, 타입스크립트가 import 문자열을 실제 파일과 선언 파일로 연결합니다.

  6. 6
    비용 상대 참조(./, ../)

    import { someFunc } from './myFile';처럼 현재 파일 기준으로 경로를 적습니다.

  7. 7
    확장성 비-상대 참조(Non-relative

    imports)는 import * as React from 'react';처럼 패키지 이름이나 별칭을 기준으로 찾습니다.

  8. 8
    예외 파일 찾기

    import 문에 지정된 이름에 해당하는 파일을 찾아야 합니다.


모듈 해석의 기본 원리

타입스크립트 컴파일러는 다음 두 가지 유형의 모듈 참조를 처리해야 합니다.

상대 참조 (./, ../): 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 컴파일러 옵션

모듈 해석은 specifier를 실제 파일과 타입 선언으로 연결한다

import 문자열이 상대 경로인지, package인지, alias인지에 따라 compiler가 탐색하는 후보가 달라진다.

질문판정
./ 또는 ../ 로 시작하나?상대 경로로 파일 후보 탐색
package 이름인가?node_modules와 package.json exports/types 확인
paths alias인가?baseUrl/paths 설정을 먼저 적용
타입만 필요한가?.d.ts와 @types 후보까지 확인
신호해석
실패 신호Cannot find module
점검 순서specifier → tsconfig → package exports → d.ts
판단런타임 해석과 타입 해석을 둘 다 맞춘다

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

주로 두 가지 전략이 사용됩니다.

"node": Node.js 환경에서 모듈을 해석하는 방식과 일치합니다.

Node.js 프로젝트(예: 백엔드 서버, CLI 도구)에서 가장 일반적으로 사용됩니다.

"bundler" (TypeScript 5.0+): 모던 번들러(Webpack, Vite, Rollup 등)의 해석 방식을 모방합니다.

웹 개발(프론트엔드) 프로젝트에서 권장됩니다.

"classic" (Legacy): 타입스크립트 초기에 사용되던 구식 전략입니다.

현재는 거의 사용되지 않습니다.

각 전략의 주요 특징을 살펴보겠습니다.

"node" 전략

node 전략은 Node.js의 require() 함수가 모듈을 찾는 방식과 동일하게 동작합니다.

  • 상대 경로: 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"라면, moduleB의 루트 폴더로 간주하고 해당 파일을 찾습니다.

    ./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 애플리케이션이나 Node.js 모듈 해석 방식을 따르는 번들러 환경에서 적합합니다.

"bundler" 전략 (TypeScript 5.0+)

bundler 전략은 Webpack, Vite, Rollup 등 현대적인 자바스크립트 번들러가 모듈을 해석하는 방식과 더 가깝습니다.

이 전략은 ES 모듈의 CJS 상호 운용성을 더 잘 모델링하고, Node.js의 "exports" 필드 지원을 개선하여 Node.js 16/ESM 호환 프로젝트에 더 적합합니다.

주요 특징
  • "exports" 필드 지원: package.json"exports" 필드를 사용하여 조건부 내보내기(conditional exports)를 더 정확하게 해석합니다. 이는 Node.js의 새로운 모듈 시스템과 웹 번들러의 동작을 통일하는 데 중요합니다.
  • type: "module" 지원: package.json"type": "module" 설정에 따라 .js 파일이 ES 모듈로 간주되는 경우를 더 정확하게 처리합니다.
  • Node.js의 main 필드 대신 exports 필드를 우선적으로 고려합니다.

이 전략은 특히 Node.js 환경에서도 ES 모듈(ESM) 구문을 사용하고자 할 때 (예: package.json"type": "module"을 설정한 프로젝트), 또는 프론트엔드 프로젝트에서 최신 번들러를 사용할 때 유용합니다.

"classic" 전략 (권장되지 않음)

classic 전략은 타입스크립트 초기에 존재하던 방식입니다.

상대 경로가 아닌 모듈을 찾을 때 node_modules를 탐색하지 않고, 단순히 현재 디렉토리와 상위 디렉토리에서 .ts, .d.ts 파일을 찾는 방식으로 동작합니다.

웹팩과 같은 번들러가 없던 시절, <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과 함께 사용하여 모듈 가져오기 경로에 대한 별칭(alias) 매핑을 정의합니다.

    복잡한 상대 경로를 줄이고 모듈 구조를 추상화하는 데 매우 유용합니다.

    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: 가상의 루트 디렉토리 목록을 정의하여, 런타임에 여러 디렉토리의 내용을 하나의 논리적인 디렉토리 구조처럼 처리할 수 있게 합니다.

    이는 빌드 시스템(번들러)이 여러 소스 디렉토리를 하나의 가상 디렉토리로 병합하는 경우에 유용합니다.

  • typeRoots: 타입 정의 파일(.d.ts)을 검색할 추가적인 디렉토리 목록을 지정합니다.

    기본적으로 node_modules/@types가 포함됩니다.

  • types: 전역 타입 선언을 포함할 특정 @types 패키지 목록을 지정합니다.

    이 목록에 없는 @types 패키지는 자동으로 포함되지 않습니다.


모듈 해석 과정 디버깅

모듈 해석이 예상대로 동작하지 않을 때는 tsc 명령어에 --traceResolution 플래그를 추가하여 자세한 해석 과정을 확인할 수 있습니다.

tsc --traceResolution

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

후보 누락이 아니라 검색 분기가 틀어진 지점을 찾는다

로그에는 정상적인 후보 실패도 많다. 대상 import의 최종 결과에서 거꾸로 읽는다.

  1. 대상 고정

    Resolving module "@/utils" from src/app.ts import 하나와 출발 파일을 고른다.

  2. 알고리즘·조건

    moduleResolution: bundler conditions: types, import 실행 환경과 같은 전략인지 본다.

  3. 분기와 후보

    paths / package exports .ts → .tsx → .d.ts 별칭·조건 뒤 후보 순서를 본다.

  4. 결과에서 역추적

    resolved to ... was not resolved 최종 판정 결과에서 예상 밖 분기로 돌아간다.

  5. Matched pattern "@/*"
    tsconfig ↔ 실제 경로

    Matched pattern "@/*" paths 는 탔지만 substitution 경로가 틀릴 수 있다. tsconfig ↔ 실제 경로

  6. conditional exports
    package.json exports

    conditional exports types · import · require 중 선택 조건을 본다. package.json exports

  7. File does not exist
    다음 후보와 최종 결과

    File does not exist 확장자 후보 한 줄만으로 원인이라고 단정하지 않는다. 다음 후보와 최종 결과


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

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

특히 최신 Node.js 환경이나 모던 번들러를 사용하는 웹 프로젝트에서는 bundler 전략을 함께 고려할 필요가 있습니다.

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

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

아래 점검표는 --traceResolution 로그를 읽을 때 어떤 단서를 먼저 봐야 하는지 압축해서 보여줍니다.

모듈을 못 찾을 때 trace 로그 읽는 순서

해석 실패는 보통 import 문자열, 해석 전략, 패키지 메타데이터, 타입 파일 위치 중 하나가 다른 방향을 가리킬 때 발생합니다.

  1. 1
    참조 종류 확인

    상대 경로는 파일 위치, 패키지는 설정 기준으로 찾습니다.

  2. 2
    전략 확인

    node , node16 , bundler 는 exports 와 확장자 규칙을 다르게 해석합니다.

  3. 3
    별칭 확인

    baseUrl과 paths 별칭은 번들러 설정도 알아야 합니다.

  4. 4
    타입 진입점 확인

    타입 진입점이 실제 JS API와 맞는지 봅니다.

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

모듈 해석 변경은 로그의 후보 파일 검증

moduleResolution 을 바꿀 때는 설정 이름보다 실제로 어떤 파일을 찾았고 건너뛰었는지 확인해야 합니다.

  1. import
    문자열

    import 상대 경로, 별칭, 패키지 이름 중 어떤 규칙으로 시작하는지 먼저 나눕니다.

  2. tsconfig
    컴파일러 기준

    tsconfig baseUrl , paths , types 가 후보를 줄이거나 넓히는지 봅니다.

  3. package
    패키지 메타

    package 패키지 메타가 타입 파일과 실행 파일을 함께 가리켜야 합니다.

  4. bundler
    도구 체인

    bundler 별칭이 빌드에서도 해석되는지 함께 맞춥니다.


다음 다이어그램은 모듈 해석 전략과 tsconfig 옵션을 경로 추적 기준으로 정리한 표입니다.

모듈 해석 설정 도구

node, bundler, classic 전략과 paths, baseUrl, traceResolution을 함께 보면 타입스크립트가 모듈을 찾는 과정을 추적할 수 있습니다.

  1. 1
    패키지 규칙

    node 전략 Node.js의 파일 확장자, package.json, node_modules 탐색 흐름을 따릅니다. moduleResolution: node

  2. 2
    번들러 친화

    bundler 전략 현대 번들러가 처리하는 경로와 package exports 흐름에 맞춥니다. bundler

  3. 3
    별칭 연결

    paths 설정 baseUrl과 paths는 import 별칭을 TypeScript 해석 규칙에 알려 줍니다. paths

  4. 4
    원인 확인

    추적 명령 traceResolution으로 어떤 후보를 찾고 왜 실패했는지 로그를 봅니다. traceResolution

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

모듈 해석 오류 추적 순서

TypeScript 컴파일러는 moduleResolution과 경로 설정을 바탕으로 import 대상의 소스, 선언 파일, 패키지 진입점을 찾습니다.

  1. 1
    import 문자열 읽기

    해석 시작 상대 경로, 절대 별칭, 패키지 이름인지에 따라 탐색 규칙이 달라집니다. import x

  2. 2
    Node 방식 선택

    옵션 moduleResolution은 Node 생태계 규칙과 최신 번들러 환경 중 어떤 기준을 따를지 정합니다. node16

  3. 3
    별칭과 기준점

    경로 매핑 baseUrl과 paths를 쓰면 짧은 import가 실제 디렉터리로 매핑됩니다. paths

  4. 4
    해석 로그 확인

    추적 traceResolution으로 어떤 후보 파일을 확인했고 왜 실패했는지 볼 수 있습니다. traceResolution

아래 다이어그램은 TypeScript 모듈 해석이 import 경로를 실제 파일과 타입 선언으로 찾는 과정을 정리합니다.

모듈 해석 전략 점검

모듈 해석은 import 문자열을 기준으로 소스 파일, package.json, 타입 선언 파일을 찾아 연결하는 규칙입니다.

  1. 상대 경로
    파일 위치에서 찾기

    상대 경로 ./ 또는 ../로 시작하면 현재 파일 기준으로 후보 확장자를 탐색합니다. ./lib/math

  2. node 전략
    Node 방식 탐색

    node 전략 node_modules, package.json, index 파일을 고려해 모듈을 찾습니다. moduleResolution: "node"

  3. bundler 전략
    현대 번들러와 정렬

    bundler 전략 package exports와 조건부 내보내기를 번들러 동작에 맞춰 해석합니다. "bundler"

  4. traceResolution
    탐색 과정 출력

    traceResolution 왜 특정 파일을 찾지 못했는지 컴파일러의 후보 경로를 확인합니다. tsc --traceResolution