TypeScript의 모듈 시스템은 ES Modules 문법을 기반으로 하며, 타입 전용 import 와 배럴 패턴 등 TypeScript만의 기능이 추가되어 있습니다.

기본 import/export

TYPESCRIPT
// user.ts — named export
export interface User {
  id: number;
  name: string;
}

export function createUser(name: string): User {
  return { id: Date.now(), name };
}

// 별도의 변수/함수를 묶어서 export
const DEFAULT_ROLE = 'user';
export { DEFAULT_ROLE };
TYPESCRIPT
// app.ts — named import
import { User, createUser, DEFAULT_ROLE } from './user';

const user: User = createUser('홍길동');

default export

TYPESCRIPT
// logger.ts
export default class Logger {
  log(message: string) {
    console.log(`[LOG] ${message}`);
  }
}

// app.ts — 이름을 자유롭게 지정
import MyLogger from './logger';
const logger = new MyLogger();

TypeScript 커뮤니티에서는 named export를 권장 합니다. default export는 import할 때 이름이 자유로워서 일관성이 떨어지고, IDE의 자동 import가 덜 정확합니다.

타입 전용 import (import type)

TypeScript 3.8부터 타입만 import할 때 명시적으로 import type을 사용할 수 있습니다.

TYPESCRIPT
// types.ts
export interface User {
  id: number;
  name: string;
}

export type Status = 'active' | 'inactive';

export const DEFAULT_STATUS: Status = 'active';
TYPESCRIPT
// app.ts — 타입만 import
import type { User, Status } from './types';

// ✅ 타입으로만 사용 가능
const user: User = { id: 1, name: '홍길동' };

// ❌ 런타임에서 값으로 사용 불가
// console.log(User); // Error — import type은 런타임에 존재하지 않음

// 값과 타입을 동시에 import할 때
import { DEFAULT_STATUS, type User as UserType } from './types';

왜 import type을 쓰나요?

  1. **번들 크기 최적화 **: import type은 컴파일 시 완전히 제거됨
  2. ** 순환 참조 방지 **: 타입만 참조하면 런타임 순환 의존성이 생기지 않음
  3. ** 의도 명확화 **: "이건 타입으로만 쓸 거야"라는 의도를 명시
TYPESCRIPT
// tsconfig.json에서 강제할 수 있음
{
  "compilerOptions": {
    "verbatimModuleSyntax": true // import type 사용을 강제
  }
}

배럴 패턴(Barrel Pattern)

여러 모듈의 export를 하나의 index.ts로 모아서 re-export하는 패턴입니다.

TYPESCRIPT
// models/user.ts
export interface User { id: number; name: string; }

// models/product.ts
export interface Product { id: number; title: string; }

// models/index.ts — 배럴 파일
export { User } from './user';
export { Product } from './product';
export type { User as UserModel } from './user'; // 별칭도 가능
TYPESCRIPT
// app.ts — 깔끔한 import
import { User, Product } from './models'; // index.ts에서 가져옴

// 배럴 없이는
// import { User } from './models/user';
// import { Product } from './models/product';

배럴 패턴의 주의점

TYPESCRIPT
// ⚠️ 큰 프로젝트에서 배럴은 트리쉐이킹을 방해할 수 있음
// 하나만 import해도 배럴의 모든 모듈이 로딩될 수 있음

// 특히 이런 와일드카드 re-export는 피하는 것이 좋음
// export * from './user';
// export * from './product';

// ✅ 명시적 re-export가 더 안전
export { User } from './user';
export { Product } from './product';

namespace (권장하지 않음)

TypeScript 초기의 모듈 시스템이지만, ES Modules이 표준이 된 지금은 거의 사용하지 않습니다.

TYPESCRIPT
// ❌ namespace — 레거시
namespace Validation {
  export function isEmail(value: string): boolean {
    return value.includes('@');
  }
}

// ✅ ES Module — 권장
export function isEmail(value: string): boolean {
  return value.includes('@');
}

단, .d.ts 파일에서 전역 타입을 선언할 때는 namespace가 여전히 사용됩니다.

모듈 해석(Module Resolution)

JSON
// tsconfig.json
{
  "compilerOptions": {
    // 경로 별칭 설정
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@models/*": ["src/models/*"],
      "@utils/*": ["src/utils/*"]
    },
    // 모듈 해석 방식
    "moduleResolution": "bundler" // Node16, NodeNext, bundler 중 선택
  }
}
TYPESCRIPT
// 경로 별칭 사용
import { User } from '@models/user';
import { formatDate } from '@utils/date';

면접 포인트: moduleResolutionnode(Node.js 전통 방식), node16(package.json exports 지원), bundler(번들러 친화적)의 차이를 알면 좋습니다.

동적 import

TYPESCRIPT
// 동적 import — 코드 스플리팅에 활용
async function loadModule() {
  const { heavyFunction } = await import('./heavy-module');
  heavyFunction();
}

// 타입만 가져올 때도 유용
type HeavyModule = typeof import('./heavy-module');

정리

  • named export를 권장하고, default export는 가능한 피한다
  • import type으로 타입만 import하면 번들 크기 최적화와 순환 참조 방지에 도움이 된다
  • 배럴 패턴(index.ts)은 import를 깔끔하게 만들지만 트리쉐이킹에 영향을 줄 수 있다
  • namespace는 레거시이므로 ES Modules을 사용한다
  • paths 별칭으로 긴 상대 경로를 줄일 수 있다
댓글 로딩 중...