본문 바로가기

모듈 해석 전략

컴파일러가 import의 경로를 실제 파일과 타입 선언으로 찾는 과정을 nodenext와 bundler, 폐기 예정인 node10과 classic 전략으로 비교하고, paths와 rootDirs, types 같은 옵션의 역할과, --traceResolution으로 해석 과정을 추적하는 방법, 검사의 성공과 실행의 성공이 갈리는 경우를 실습합니다.

import { double } from "./utils/math.js"를 만나면 컴파일러는 그 문자열이 가리키는 파일을 찾아, 그 안의 타입으로 이 파일을 검사합니다. 이 과정을 모듈 해석(module resolution)이라고 합니다. 그런데 출력한 자바스크립트를 실행할 때 그 파일을 찾는 것은 컴파일러가 아니라 Node.js나 번들러이므로, 두 쪽이 같은 규칙으로 같은 파일을 찾아야 검사와 실행이 함께 성공합니다.

이 글은 컴파일러가 경로를 찾는 전략과 그 전략을 바꾸는 옵션을 정리하고, --traceResolution으로 해석 과정을 직접 확인합니다. 마지막으로 검사는 성공했는데 실행은 실패하는 구성을 만들어, 두 규칙을 맞추는 방법을 실습합니다.

상대 경로와 비상대 경로

가져오기의 경로는 크게 두 종류입니다. ./나 ../로 시작하는 상대 경로(relative path)는 지금 파일이 있는 폴더를 기준으로 다른 파일을 찾고, react나 @utils/math처럼 그렇지 않은 비상대 경로(non-relative path)는 paths의 별칭이나 node_modules에 설치된 패키지에서 찾습니다.

어느 쪽이든 컴파일러가 찾는 것은 실행할 파일 자체가 아니라 타입 정보입니다. 그래서 .js 파일을 가리키는 경로라도, 그에 대응하는 .ts나 .d.ts 선언 파일을 찾아 검사합니다. 패키지라면 package.json의 exports나 types 필드가 가리키는 선언 파일을 찾고, 선언 파일이 없으면 @types 패키지를 찾습니다.

moduleResolution 전략

moduleResolution 옵션은 경로를 파일로 바꾸는 규칙을 정합니다. 지금 쓰는 전략은 실행 환경에 따라 두 가지로 나뉩니다.

전략쓰는 곳특징
nodenextNode.js가 출력한 파일을 직접 실행할 때ES 모듈과 CommonJS의 규칙을 구분하고, ES 모듈의 상대 경로에는 확장자를 요구한다
bundler번들러가 출력을 다시 묶을 때exports 필드를 따르면서 상대 경로의 확장자 생략과 폴더의 index 파일을 허용한다
node10예전의 CommonJS 프로젝트exports 필드를 모르며, TypeScript 6.0에서 폐기 예정이다
classic초기 타입스크립트node_modules를 찾지 않으며, TypeScript 6.0에서 폐기 예정이다

nodenext는 앞의 두 글에서 썼듯이 파일마다 ES 모듈인지 CommonJS인지를 package.json의 "type"과 확장자로 정하고, 그 형식의 Node.js 규칙대로 찾습니다. module을 nodenext로 두면 moduleResolution도 저절로 nodenext가 됩니다.

bundler는 Vite나 webpack 같은 번들러가 실제로 하는 일에 맞춘 전략입니다. 패키지의 exports 필드는 Node.js처럼 따르지만, ./utils/math처럼 확장자를 뺀 상대 경로도 받아 줍니다. 대신 이 전략으로 검사한 코드를 Node.js가 직접 실행하면 확장자가 없는 경로를 찾지 못하므로, 출력을 정말 번들러가 처리하는 프로젝트에서만 고릅니다.

node10은 예전 Node.js의 CommonJS 탐색을 흉내 내는 전략으로, ./moduleA를 만나면 moduleA.ts, moduleA.tsx, moduleA.d.ts를 차례로 찾고, 없으면 moduleA 폴더의 index 파일을 찾습니다. 패키지의 exports 필드를 모르기 때문에 요즘 패키지와 맞지 않는 경우가 많아, TypeScript 6.0에서 classic과 함께 폐기 예정으로 지정되었습니다. 오래된 설정에서 이 값을 만나면 nodenext나 bundler로 옮깁니다.

모듈 해석이 의존하는 표면 — 상태 추적

독립 폴더 실습에서 paths alias는 검사 통과 뒤 출력 문자열이 남아 Node에서 실행이 실패하고, 교정한 상대 .js 경로는 14를 출력한다. 구성/산출/실행 결과의 같은 역할로 두 실습을 비교할 가치가 있다.

실행 환경에 맞춘 NodeNext 또는 bundler, 패키지 공개 경로, paths 출력 보존, 전역 타입 옵션과 명시적 모듈 import의 차이를 비교한다.

TYPE / RUNTIME / CONTRACT

타입 파일을 찾는 규칙과 실행 파일을 찾는 규칙을 맞춘다

TypeScript는 import 문자열을 해석해 타입을 검사합니다. 출력 JavaScript의 같은 경로를 Node나 번들러가 찾는지는 별도로 확인해야 합니다.

타입 파일을 찾는 규칙과 실행 파일을 찾는 규칙을 맞춘다위는 실행 별칭이 없는 독립 폴더에서 paths만 지정한 실습이고, 아래는 NodeNext와 상대 .js 경로로 교정한 실습입니다. 두 영역은 독립적이며 컴파일러가 파일을 찾았다는 사실만으로 Node가 같은 문자열을 찾을 수 있다고 결론내리지 않습니다.별칭만 만든 구성가져오기@utils/math타입 해석src/utils/math.tsJS 출력@utils/math문자열 그대로Node 실행패키지 없음실행 실패직접 실행 경로 교정가져오기./utils/math.js타입 해석src/utils/math.tsJS 출력./utils/math.jsNode 실행14 출력
타입 파일을 찾는 규칙과 실행 파일을 찾는 규칙을 맞춘다위는 실행 별칭이 없는 독립 폴더에서 paths만 지정한 실습이고, 아래는 NodeNext와 상대 .js 경로로 교정한 실습입니다. 두 영역은 독립적이며 컴파일러가 파일을 찾았다는 사실만으로 Node가 같은 문자열을 찾을 수 있다고 결론내리지 않습니다.별칭만 만든 구성가져오기@utils/math타입 해석src/utils/math.tsJS 출력@utils/math문자열 그대로Node 실행패키지 없음실행 실패직접 실행 경로 교정가져오기./utils/math.js타입 해석src/utils/math.tsJS 출력./utils/math.jsNode 실행14 출력

위는 실행 별칭이 없는 독립 폴더에서 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/mathpaths로 @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를 출력한다. 구성/산출/실행 결과의 같은 역할로 두 실습을 비교할 가치가 있다.

실행 환경에 맞춘 NodeNext 또는 bundler, 패키지 공개 경로, paths 출력 보존, 전역 타입 옵션과 명시적 모듈 import의 차이를 비교한다.

TYPE / RUNTIME / CONTRACT

타입 파일을 찾는 규칙과 실행 파일을 찾는 규칙을 맞춘다

TypeScript는 import 문자열을 해석해 타입을 검사합니다. 출력 JavaScript의 같은 경로를 Node나 번들러가 찾는지는 별도로 확인해야 합니다.

타입 파일을 찾는 규칙과 실행 파일을 찾는 규칙을 맞춘다위는 실행 별칭이 없는 독립 폴더에서 paths만 지정한 실습이고, 아래는 NodeNext와 상대 .js 경로로 교정한 실습입니다. 두 영역은 독립적이며 컴파일러가 파일을 찾았다는 사실만으로 Node가 같은 문자열을 찾을 수 있다고 결론내리지 않습니다.별칭만 만든 구성가져오기@utils/math타입 해석src/utils/math.tsJS 출력@utils/math문자열 그대로Node 실행패키지 없음실행 실패직접 실행 경로 교정가져오기./utils/math.js타입 해석src/utils/math.tsJS 출력./utils/math.jsNode 실행14 출력
타입 파일을 찾는 규칙과 실행 파일을 찾는 규칙을 맞춘다위는 실행 별칭이 없는 독립 폴더에서 paths만 지정한 실습이고, 아래는 NodeNext와 상대 .js 경로로 교정한 실습입니다. 두 영역은 독립적이며 컴파일러가 파일을 찾았다는 사실만으로 Node가 같은 문자열을 찾을 수 있다고 결론내리지 않습니다.별칭만 만든 구성가져오기@utils/math타입 해석src/utils/math.tsJS 출력@utils/math문자열 그대로Node 실행패키지 없음실행 실패직접 실행 경로 교정가져오기./utils/math.js타입 해석src/utils/math.tsJS 출력./utils/math.jsNode 실행14 출력

위는 실행 별칭이 없는 독립 폴더에서 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/mathpaths로 @utils/math를 src 파일로 해석한다paths는 출력 문자열을 다시 쓰지 않는다
alias 실행 실패Node: 해석 실패 / 타입 검사: 성공과 별개별칭 loader가 없는 독립 Node 실행은 패키지를 찾지 못한다컴파일러의 파일 발견과 runtime 로딩을 구분한다
상대 .js 경로 교정출력: ./utils/math.js / Node 결과: 14다른 독립 실습은 ./utils/math.js를 출력하고 14를 얻는다두 구성을 같은 runtime에서 연속으로 자동 수정한 것으로 읽지 않는다

위 그림의 위쪽은 이 글의 첫 실습처럼 paths 별칭만 만든 구성이고, 아래쪽은 Node.js의 규칙에 맞추어 고친 구성입니다. 컴파일러가 파일을 찾았다는 사실만으로 실행 환경도 같은 문자열을 찾을 수 있다고 결론 내리지 않는다는 것이 두 구성을 비교하는 이유입니다.

해석에 영향을 주는 옵션

paths는 비상대 경로를 컴파일러가 찾을 위치에 이어 줍니다. 7장 1편에서 본 것처럼 TypeScript 6.0에서는 baseUrl 없이 "@utils/*": ["./src/utils/*"]처럼 각 경로에 기준 위치를 직접 적습니다. 중요한 점은 paths가 컴파일러의 검사에만 쓰이고, 출력한 import의 경로를 바꾸지 않는다는 것입니다.

rootDirs는 여러 폴더가 실행할 때 하나로 합쳐진다고 가정하고, 그 폴더들 사이의 상대 경로를 검사하게 합니다. 빌드 도구가 소스 폴더와 생성된 코드의 폴더를 한곳에 모으는 구성에서 쓰며, 실제로 폴더를 합쳐 주지는 않습니다.

types와 typeRoots는 import하지 않아도 전역에 포함할 @types 패키지를 정합니다. TypeScript 6.0에서 types의 기본값은 빈 배열이어서, Node.js의 전역인 process를 쓰려면 "types": ["node"]처럼 직접 적어야 합니다. 이 옵션은 전역 선언만 다루므로, 코드에서 import한 패키지의 타입을 찾는 일은 막지 않습니다.

해석 과정 추적하기

모듈을 찾지 못하는 진단이 나오면 --traceResolution을 붙여 컴파일러가 어떤 후보를 찾아보았는지 확인합니다. 출력은 길지만, 경로마다 ======== Resolving module으로 시작해 was successfully resolved to나 was not resolved로 끝나는 덩어리로 나뉘어 있습니다.

새 폴더 ts-resolution을 만들고 package.json을 { "type": "module" }로 둡니다. 실행할 때 별칭을 풀어 줄 번들러나 도구는 설치하지 않습니다.

ts-resolution/
├── package.json
├── tsconfig.json
└── src/
    ├── app.ts
    └── utils/
        └── math.ts
src/utils/math.ts
export function double(value: number): number {
  return value * 2;
}
src/app.ts
import { double } from "@utils/math";
console.log(double(7));
tsconfig.json
{
  "compilerOptions": {
    "target": "es2022",
    "module": "esnext",
    "moduleResolution": "bundler",
    "rootDir": "src",
    "outDir": "dist",
    "strict": true,
    "paths": { "@utils/*": ["./src/utils/*"] }
  },
  "include": ["src"]
}
tsc -p . --traceResolution
node dist/app.js

추적 출력에서 @utils/math의 덩어리를 찾으면, paths의 @utils/* 패턴에 맞아 ./src/utils/math를 후보로 삼고, 결국 src/utils/math.ts로 해석되었다는 줄이 나옵니다. 그래서 컴파일은 진단 없이 성공합니다. 하지만 dist/app.js의 가져오기는 여전히 "@utils/math"이고, Node.js는 이 문자열을 @utils/math라는 패키지로 찾다가 없으므로 ERR_MODULE_NOT_FOUND 오류로 멈춥니다. 14는 출력되지 않으며, bundler를 골랐다는 사실이 번들러를 설치하거나 실행해 주는 것도 아닙니다.

이 구성을 Node.js에서 직접 실행하려면 두 가지를 함께 바꿉니다. tsconfig.json의 module과 moduleResolution을 nodenext로 바꾸고, app.ts의 가져오기를 출력 파일의 상대 경로인 "./utils/math.js"로 고칩니다.

tsc -p .
node dist/app.js
14

이제 컴파일러는 ./utils/math.js에 대응하는 src/utils/math.ts를 검사하고, Node.js는 같은 문자열로 dist/utils/math.js를 찾습니다. 두 쪽이 같은 규칙으로 같은 파일을 가리키게 된 것입니다. paths가 출력을 바꾸지 않는 이유는 공식 모듈 참조에 설명되어 있습니다.

더 깊이: 실행 환경도 아는 별칭, 하위 경로 가져오기

긴 상대 경로를 줄이고 싶지만 번들러가 없다면, paths 대신 Node.js의 하위 경로 가져오기(subpath imports)를 쓸 수 있습니다. package.json의 imports 필드에 "#utils/*": "./dist/utils/*"처럼 별칭을 적으면, 그 패키지 안의 코드는 import { double } from "#utils/math.js"로 가져올 수 있습니다. Node.js의 패키지 문서에 따르면 이 이름은 바깥 패키지 이름과 구별되도록 반드시 #으로 시작해야 하고, 그 패키지 안에서만 쓰는 비공개 연결입니다. 이 연결은 Node.js가 실행할 때 직접 풀기 때문에, 출력을 바꾸지 않는 paths와 달리 실행 환경도 같은 별칭을 이해합니다.

타입스크립트도 moduleResolution이 nodenext나 bundler일 때 imports 필드를 읽어 같은 별칭을 해석합니다. 예전에는 # 바로 뒤에 /가 올 수 없어서 #root/처럼 의미 없는 이름을 하나 더 붙여야 했는데, Node.js가 v24.14.0에서 #/로 시작하는 하위 경로를 허용했고 TypeScript 6.0도 같은 두 설정에서 이를 지원합니다. 그래서 번들러에서 익숙한 @/ 대신 "#/*": "./dist/*"처럼 짧은 접두어를 쓸 수 있습니다.

정리

  • 모듈 해석은 import의 경로를 타입 정보가 있는 파일로 찾는 과정이며, 실행할 때 파일을 찾는 것은 Node.js나 번들러입니다.
  • Node.js가 직접 실행할 코드는 nodenext로, 번들러가 다시 묶을 코드는 bundler로 검사하며, node10과 classic은 TypeScript 6.0에서 폐기 예정입니다.
  • paths는 컴파일러의 검사에만 쓰이고 출력한 경로를 바꾸지 않으므로, 같은 별칭을 실행 환경도 알아야 합니다.
  • TypeScript 6.0에서 types의 기본값은 빈 배열이어서, 전역 선언이 필요한 @types 패키지는 직접 적습니다.
  • --traceResolution으로 컴파일러가 찾아본 후보와 결과를 확인하고, 검사의 성공과 실행의 성공은 따로 확인합니다.

연습 문제

1. 파일 이름을 바꾼 실행

Node.js로 직접 실행하는 구성에서 math.ts를 helper.ts로 옮기고, 호출하는 값을 double(9)로 바꾸었습니다. 가져오기 문자열은 무엇이 되어야 하고 출력은 무엇인가요? 파일만 옮기고 문자열을 그대로 두면 어느 단계에서 문제가 드러나나요?

해설 보기

가져오기 문자열은 "./utils/helper.js"이고 출력은 18입니다. 문자열을 "./utils/math.js"로 그대로 두면, 컴파일러가 대응하는 소스를 찾지 못해 Cannot find module './utils/math.js' or its corresponding type declarations.라는 TS2307이 컴파일 단계에서 나옵니다. 예전에 만든 dist/utils/math.js가 남아 있으면 실행은 될 수도 있으므로, 이런 변형은 새 폴더에서 해 보는 것이 안전합니다.

2. 별칭을 쓰면서 Node.js에서 실행하기

@utils/math 같은 별칭을 소스에 그대로 두면서 Node.js에서 직접 실행하고 싶다면, paths 말고 무엇을 맞추어야 하나요?

해설 보기

Node.js가 그 별칭을 알도록 package.json의 imports 필드에 패키지 안의 경로를 정해 주는 방법이 있습니다. Node.js의 imports는 이름이 #으로 시작해야 하므로 별칭도 #utils/math.js처럼 바꾸어야 하고, nodenext와 bundler 전략은 이 필드를 읽어 해석합니다. TypeScript 6.0부터는 #/로 시작하는 이름도 해석하지만, 실행할 Node.js가 그 형식을 지원하는지도 확인해야 하며, 어느 쪽이든 컴파일러와 실행 환경이 같은 표를 보게 만드는 것이 핵심입니다.

3. 전역 타입을 찾지 못할 때

TypeScript 6.0으로 옮긴 뒤 console.log(process.env.HOME)에서 Cannot find name 'process'.가 나왔습니다. @types/node는 설치되어 있습니다. 왜 그런가요?

해설 보기

TypeScript 6.0에서 types의 기본값이 빈 배열로 바뀌어, 설치된 @types 패키지를 더는 전역에 자동으로 포함하지 않기 때문입니다. process는 @types/node가 전역에 선언하는 이름이므로, tsconfig.json에 "types": ["node"]를 적어야 합니다. "node:process"나 "fs" 같은 Node.js 내장 모듈도 @types/node가 전역에 선언해 두는 모듈이어서, import process from "node:process";로 바꾸어도 types에 node가 없으면 그 모듈을 찾지 못한다는 진단이 나옵니다.

이것으로 7장의 모듈과 네임스페이스를 마칩니다. 다음 장의 .d.ts 파일 작성법에서는 이 글에서 컴파일러가 찾던 타입 선언 파일을 직접 쓰는 방법을 다룹니다.