본문으로 건너뛰기
김신건의 로그

[TypeScript] Modules (import/export, moduleResolution)

· 수정 · 📖 약 3분 · 926자/단어 #typescript #modules #esm #cjs
TypeScript Modules, TS Modules, TypeScript import, TypeScript export, moduleResolution, nodenext, verbatimModuleSyntax, TS 모듈, isolatedModules

정의

TypeScript 모듈 시스템은 ES Modules (ESM) 를 기본 언어 모델로 삼되, CommonJS (CJS) 를 상호운용성 (interop) 계층으로 지원합니다. moduleResolution 옵션이 어떻게 파일을 찾을지, module 이 어떻게 emit 할지를 결정하며, TypeScript 5.0 의 verbatimModuleSyntax 는 마술을 최소화합니다.

import / export 기본

Named export

// utils.ts
export function add(a: number, b: number): number { return a + b; }
export function sub(a: number, b: number): number { return a - b; }
export const PI = 3.14;

// main.ts
import {add, sub, PI} from "./utils";

Default export

// user.ts
export default class User {
  constructor(public name: string) {}
}

// main.ts
import User from "./user";

default 는 남용 지양. Named export 가 명확 (rename 안전, tree-shaking 좋음).

Re-export

// index.ts (barrel)
export * from "./utils";
export * from "./models";
export {default as User} from "./user";
export type {ApiResponse} from "./types";

Namespace import

import * as utils from "./utils";
utils.add(1, 2);

Side effect import

import "./polyfill";   // 부수효과만

Type-only import / export

Type 만 import 하면 컴파일 시 제거됨 -> 런타임 부담 없음.

import type {User} from "./types";
import type {ApiResponse, ApiError} from "./api";

// 함수 안에서 타입만 사용
function process(u: User): string {
  return u.name;
}

혼합:

import {logger, type LoggerOptions} from "./logger";

verbatimModuleSyntax: true (TS 5.0+)

TS 5.0+ 권장. type 없는 import 는 실제 런타임에 유지, type 은 제거. 마술 없음.

// verbatimModuleSyntax: true
import {LoggerOptions} from "./logger";       // LoggerOptions 가 값이면 유지, 타입만이면 오류
import type {LoggerOptions} from "./logger";  // 확실히 타입만

모듈 형식 (module)

tsconfig.jsonmodule 옵션은 emit 형식:

emit
commonjsrequire, module.exports
esnext / es2020 / es2022ES modules (import, export)
nodenext / node16파일 확장자 / package.json 에 따라 자동
amd, umd, system브라우저 legacy
preserve (TS 5.4+)원본 그대로 (esbuild 등에게 위임)

모듈 해석 (moduleResolution)

파일 경로를 어떻게 찾을지:

특성
node (legacy)Node.js require 알고리즘 (CJS)
node16 / nodenextNode 16+ ESM 지원 (package.json type, exports 존중)
bundler (TS 5.0+)번들러 (Webpack, Vite, esbuild) 규칙, ESM 형식 파일 확장자 없음
classic (legacy)매우 오래된 스타일, 사용 안 함

bundler (권장, 웹앱)

Vite, Next.js, Webpack, esbuild 등이 실제 해석. TS 는 검사만.

{
  "compilerOptions": {
    "module": "esnext",
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,     // .ts 확장자 명시 허용
    "noEmit": true                            // 번들러가 emit
  }
}

nodenext (Node.js 앱)

Node.js 16+ 의 실제 ESM 규칙 준수:

  • .mjs / .cjs 확장자
  • package.json"type": "module" / "type": "commonjs"
  • exports 필드 (conditional exports)
  • ESM 에서는 파일 확장자 명시 필수 (import "./util.js", 실제 파일은 .ts 여도 .js)
{
  "compilerOptions": {
    "module": "nodenext",
    "moduleResolution": "nodenext"
  }
}

.js 확장자 문제

nodenext / node16 하에서 ESM import 는 컴파일된 파일 이름 (.js) 을 참조:

// src/main.ts
import {util} from "./util.js";   // 실제 파일은 util.ts, 컴파일 후 util.js

// 아래는 오류
import {util} from "./util";
import {util} from "./util.ts";

TypeScript 5.0+ allowImportingTsExtensions.ts 도 명시 가능 (하지만 emit 은 안 함, 번들러에게 위임).

Path Aliases (paths)

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"]
    }
  }
}
import {Button} from "@/components/Button";
import type {User} from "@/types";

중요: TypeScript 는 검사만. 런타임 (또는 번들러) 도 같은 alias 해석 필요:

  • Vite: resolve.alias 자동으로 tsconfig 읽음 (vite-tsconfig-paths 플러그인)
  • Webpack: resolve.alias 수동
  • Next.js: tsconfig.json 자동
  • Node.js (러ntime): tsconfig-paths 폴리필 필요

isolatedModules

{"isolatedModules": true}

각 파일이 독립적으로 컴파일 가능해야 함 (esbuild/swc/Babel 이 파일 단위로 트랜스파일 하기 때문). 활성화 시:

  • const enum 사용 시 오류 (다른 파일에서 참조 불가) -> Union literal 로 대체
  • default export re-export: export {default} from "./x" 는 OK, export default from "./x" 는 불가
  • 네임스페이스 (namespace) 로 인한 컴파일 부작용 감지

관용: isolatedModules: true 는 프로덕션 필수.

esModuleInterop

CJS 모듈을 ESM 스타일로 import 하도록:

{"esModuleInterop": true}
// esModuleInterop: false
import * as express from "express";
const app = express();   // 오류: express 는 default export 아님

// esModuleInterop: true
import express from "express";
const app = express();   // OK, TypeScript 가 자동 wrap

allowSyntheticDefaultImports

esModuleInterop 을 활성화하면 자동. 명시적으로 켤 수도.

Namespace (legacy)

namespace Utils {
  export function add(a: number, b: number): number { return a + b; }
}

Utils.add(1, 2);

Deprecated 스타일. ES modules 등장 이전의 module 시스템. 새 코드에서는 사용 안 함.

Module 안의 Ambient Declaration

// global.d.ts
declare global {
  interface Window {
    myGlobal: string;
  }
}

// 사용
window.myGlobal;

Ambient 는 Declaration Files 참조.

import.meta

ESM 의 표준. Module URL 등 접근.

console.log(import.meta.url);   // "file:///.../main.js"

// Node.js ESM
if (import.meta.url === `file://${process.argv[1]}`) {
  main();
}

Vite:

if (import.meta.env.DEV) { ... }
if (import.meta.hot) { ... }   // HMR

Dynamic import

const module = await import("./heavy");
module.foo();

Code splitting 의 기본. Type:

type ModuleType = typeof import("./heavy");
const module: ModuleType = await import("./heavy");

실전 tsconfig (2026 표준)

Bundler (Web app, Next.js, Vite)

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "esnext",
    "moduleResolution": "bundler",
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "strict": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "lib": ["ES2022", "DOM"],
    "paths": {"@/*": ["src/*"]}
  },
  "include": ["src/**/*"]
}

Node.js (backend, CLI)

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "isolatedModules": true,
    "verbatimModuleSyntax": true,
    "esModuleInterop": true,
    "strict": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "declaration": true,
    "sourceMap": true,
    "lib": ["ES2022"]
  },
  "include": ["src/**/*"]
}

함정

WARNING

nodenext 하에서 확장자 필수. import "./util" 은 오류. import "./util.js". 이는 TS 컴파일 후 .js 파일을 찾기 때문.

CAUTION

isolatedModules 없이 esbuild/swc 사용 위험. Const enum, namespace 등이 조용히 잘못 컴파일.

WARNING

paths 는 runtime 해석에 자동 반영 안 됨. 번들러 / Node.js 도 같은 alias 해석 지원 필요.

IMPORTANT

verbatimModuleSyntax 를 켜세요. TS 5.0+ 강력 권장. Emit 결과의 예측성 향상.

CAUTION

CJS <-> ESM 경계. Node.js 에서 CJS 파일이 ESM import 하려면 dynamic import 필요. require("esm-only-package") 는 오류.

WARNING

Barrel file (index.ts) 남용. Tree-shaking 저해, 빌드 느려짐, 순환 import. 큰 앱에서는 지양.

관련 위키

이 글의 용어 (5개)
[Javascript] ES Modules (import / export)javascript
정의 ES Modules (ESM) 은 표준 JS 모듈 시스템 (ES6). / 키워드. 브라우저와 Node.js 16+ 모두 지원. export named export defau…
[TypeScript] Declaration Files (.d.ts)typescript
정의 Declaration File ( ) 은 타입 정보만 담은 파일입니다. 실행 코드 없이 함수, 클래스, 변수, 모듈의 타입 시그니처 만 선언합니다. JavaScript 라이…
[TypeScript] Decoratorstypescript
정의 Decorator 는 class, method, accessor, property, parameter 에 부착되는 함수로 그 대상을 검사/수정할 수 있습니다. TypeScr…
[TypeScript] Strict Mode & tsconfigtypescript
정의 Strict Mode 는 여러 엄격한 타입 검사 플래그를 한 번에 켜는 옵션 ( ) 입니다. 강력한 타입 안전성을 제공하지만, 기존 코드베이스에는 대량의 오류를 발생시킬 수…
TypeScripttypescript
정의 TypeScript 는 Microsoft 가 2012년 발표한 JavaScript 의 상위 집합 프로그래밍 언어입니다. 정적 타입 시스템, 인터페이스, 제네릭, enum, …

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기