TypeScript 모듈은 최상위에 import나 export가 하나 이상 있는 파일입니다. 그 안에서 선언한 것은 export하지 않는 한 파일 안에서만 보이고, 다른 파일은 필요한 것을 import합니다. 문법은 JavaScript의 ES 모듈 문법에 타입 전용 형태 몇 가지를 더한 것입니다.
named export와 default export
프로젝트에는 파일이 여러 개 있으므로, import하는 쪽은 다음과 같습니다. default export는 중괄호 없이 가져오며 이름을 마음대로 붙일 수 있습니다. named export는 중괄호 안에 쓰며, as로 바꾸지 않는 한 이름이 그대로 유지됩니다.
// main.ts
import describe, { distance, ORIGIN, type Point } from "./math.js";
import { distance as dist } from "./math.js"; // renamed on import
import * as math from "./math.js"; // everything, as one object
const p: Point = { x: 6, y: 8 };
console.log(describe(p), distance(ORIGIN, p), dist(p, p), math.ORIGIN);
| 형태 | 가져오는 것 |
|---|---|
import { a, b } from "./m.js" | named export a와 b |
import x from "./m.js" | default export를 x라는 이름으로 |
import * as m from "./m.js" | 모든 export를 담은 네임스페이스 객체 |
import { a as b } from "./m.js" | a를 이 파일에서 b로 이름을 바꿔서 |
import type { T } from "./m.js" | 타입만, 출력에서 제거됨 |
import "./setup.js" | 부수 효과를 위해 파일을 실행 |
export { a } from "./m.js" | a를 import하지 않고 다시 export |
export * from "./m.js" | 모든 named export를 다시 export |
다시 export(re-export)하면 한 파일(주로 index.ts)이 폴더의 공개 API를 모을 수 있습니다. 라이브 바인딩이나 모듈 캐싱 같은 순수 JavaScript 모듈 동작은 ES 모듈에서 다룹니다.
import type과 export type
타입은 실행 시점에 존재하지 않으므로, 타입으로만 쓰는 import는 불러올 것이 없습니다. import type은 이를 명시하며, 이 문장은 JavaScript 출력에서 사라집니다. type 수식어는 일반 import 안의 이름 하나에도 붙일 수 있습니다.
import type { User } from "./models.js"; // whole statement erased
import { saveUser, type Settings } from "./api.js"; // only saveUser survives
export type { User }; // re-export a type only
export type UserId = User["id"];
키워드가 없어도 TypeScript는 타입으로만 쓰인다고 판단되는 이름을 제거합니다. 다만 다음 두 설정에서는 키워드가 필수입니다.
verbatimModuleSyntax: true는type으로 표시하지 않은 import를 모두 남기므로, 표시하지 않은 타입 import는 TS1484 오류'Point' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.가 됩니다.- Node에서
.ts파일을 직접 실행하면(타입 제거) 타입 표기는 지우지만 다른 파일은 보지 않습니다. 표시하지 않은import { Point }는 코드에 남고, Node는 실행 시점에SyntaxError: The requested module './math.ts' does not provide an export named 'Point'로 실패합니다.
타입 전용 import마다 type을 쓰면 두 경우 모두 동작하므로, 습관으로 들여 두세요.
Cannot Find Module
import 경로가 파일이나 타입이 있는 패키지로 이어지지 않으면 컴파일러는 TS2307 오류로 멈춥니다. 다음 코드를 실행해 확인해 보세요.
출력은 index.ts(2,29): error TS2307: Cannot find module './utils.js' or its corresponding type declarations.입니다. 흔한 원인은 다음과 같습니다.
- 상대 경로의 오타, 또는
./누락(없으면 이름을node_modules에서 찾습니다). - 타입이 번들되지 않은 JavaScript 패키지:
@types/{package}가 있으면 설치하고, 없으면 선언 파일을 직접 작성하세요. - 패키지의
package.json에 있는exports필드에 나열되지 않은 하위 경로.node16,nodenext,bundler해석에서는 나열된 진입점만 import할 수 있습니다.
ES 모듈 출력과 CommonJS 출력
코드는 언제나 import와 export로 씁니다. 어떤 JavaScript가 나올지는 tsconfig.json의 module 옵션이 정합니다.
// Source, the same in both cases
import { add } from "./math.js";
console.log(add(1, 2));
// module: nodenext, in a package with "type": "module"
import { add } from "./math.js";
console.log(add(1, 2));
// module: nodenext, in a package without "type": "module"
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const math_js_1 = require("./math.js");
console.log((0, math_js_1.add)(1, 2));
node16, node18, node20, nodenext에서 TypeScript는 파일마다 Node 자체의 규칙을 따릅니다.
| 파일 | 출력 형식 |
|---|---|
"type": "module"이 있는 패키지의 .ts | ES 모듈 |
그것이 없는 패키지의 .ts | CommonJS |
.mts | 항상 ES 모듈, .mjs로 출력 |
.cts | 항상 CommonJS, .cjs로 출력 |
module: "esnext"나 "preserve"에서는 출력이 import/export를 유지하며, Vite와 esbuild 같은 번들러가 기대하는 형태입니다. 두 모듈 시스템의 실행 시점 차이는 CommonJS와 ESM 비교에 있습니다.
import의 파일 확장자
module: node16이나 nodenext에서 ES 모듈 파일은 Node가 실제로 불러올 파일, 즉 컴파일된 .js 파일의 이름을 써야 합니다. TypeScript는 타입 검사를 위해 ./math.js를 다시 math.ts로 매핑합니다.
import { add } from "./math"; // error TS2835 in an ES module file
import { add } from "./math.js"; // correct: the path as it exists after compiling
오류 메시지는 Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './math.js'?입니다. 같은 설정에서도 CommonJS 파일은 확장자를 생략할 수 있는데, require가 직접 확장자를 시도하기 때문입니다.
다음 두 설정에서는 규칙이 달라집니다.
moduleResolution: "bundler"(module이esnext,preserve,commonjs일 때)는 번들러가 해석하므로 확장자 없는./math를 받아들입니다.rewriteRelativeImportExtensions: true를 켜면 Node가.ts파일을 직접 실행할 때도 통하는 경로인./math.ts로 쓸 수 있고, 출력에서는./math.js로 바뀝니다.
모듈 해석과 paths
"zod" 같은 패키지 이름이라면 TypeScript는 node_modules를 찾아 패키지의 package.json(exports와 types 필드)을 읽고, 없으면 node_modules/@types/zod를 봅니다. 상대 이름(./, ../)은 import하는 파일을 기준으로 해석합니다. .json 파일도 import할 수 있으며, 필요한 설정은 JSON 페이지에 있습니다.
tsconfig.json의 paths는 자신의 폴더에 별칭을 추가합니다.
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"paths": {
"@lib/*": ["./src/lib/*"]
}
}
}
paths는 타입 검사에만 영향을 줍니다. 출력된 파일에는 여전히 import { v } from "@lib/util"이 남으므로, 실행 시점에 다른 무언가가 이를 해석해야 합니다. 같은 별칭을 설정한 번들러나, 추가 도구 없이 동작하는 package.json의 Node imports 필드("#lib/*": "./dist/lib/*"처럼 #로 시작하는 별칭)가 그 역할을 합니다. baseUrl은 TypeScript 7에서 사라졌으니(TS5102 오류), paths 항목은 tsconfig.json 기준 상대 경로로 앞에 ./를 붙여 쓰세요.
자주 묻는 질문
TypeScript에서 import와 import type의 차이는 무엇인가요?
import type { User } from "./user.js"는 타입만 가져올 수 있으며, 문장 전체가 JavaScript 출력에서 제거됩니다. 일반 import는 값과 타입을 모두 가져올 수 있습니다. TypeScript는 타입으로만 쓰인 이름을 제거하지만, verbatimModuleSyntax를 켜면 출력이 작성한 그대로가 되도록 그런 이름에 type을 표시하라고 요구합니다.
TypeScript는 왜 import 경로에 .js를 요구하나요?
module: node16이나 nodenext에서 ES 모듈 파일은 Node가 실행 시점에 불러올 실제 파일 이름으로 import해야 하고, 그 파일은 컴파일된 .js입니다. TypeScript는 타입 검사 중에 ./math.js를 math.ts로 해석합니다. ES 모듈 파일에서 확장자를 빼면 TS2835 오류입니다.
TypeScript에서 export default와 named export 중 무엇을 써야 하나요?
둘 다 동작합니다. 많은 팀이 named export를 선호합니다. import하는 모든 파일에서 이름이 같고, 에디터가 안정적으로 자동 import하며, 이름 변경이 검색이 아니라 리팩터링이 되기 때문입니다. default export는 import하는 쪽마다 이름을 고를 수 있습니다.
tsconfig의 paths 옵션은 출력의 import를 바꾸나요?
아니요. paths는 타입 검사기에게 모듈을 어디서 찾을지만 알려 줍니다. 출력된 JavaScript에는 "@lib/util"이 작성한 그대로 남으므로, 번들러나 package.json에 있는 Node의 imports 필드가 실행 시점에 이를 해석해야 합니다.
TypeScript에서 "Cannot find module" 오류는 어떻게 고치나요?
TS2307 오류는 경로가 TypeScript가 찾을 수 있는 파일을 가리키지 않거나, 패키지에 타입이 없다는 뜻입니다. 상대 경로와 확장자를 확인하고, 패키지에 내장 타입이 없다면 @types/... 패키지를 설치하거나 작은 선언 파일을 직접 작성하세요.