8.3 모듈 보강(Module Augmentation) — 기존 타입 확장하기
모듈 보강이란?
**모듈 보강(Module Augmentation)**은 이미 존재하는 모듈의 타입 정의를 수정하거나 확장하는 기법입니다. 라이브러리 소스를 건드리지 않고 타입을 추가할 수 있습니다.
// 기존 express 모듈에 사용자 정보 타입 추가
import 'express';
declare module 'express' {
interface Request {
user?: AuthUser; // 기존 Request에 user 프로퍼티 추가
sessionId?: string;
}
}
왜 모듈 보강이 필요한가?
문제 상황
import express, { Request, Response } from 'express';
// JWT 미들웨어 구현
function authMiddleware(req: Request, res: Response, next: Function) {
const token = req.headers.authorization;
const user = verifyToken(token);
req.user = user; // ❌ Property 'user' does not exist on type 'Request'
next();
}
해결: 모듈 보강
// src/types/express.d.ts
import { AuthUser } from '../models/user';
declare module 'express-serve-static-core' {
interface Request {
user?: AuthUser;
}
}
// 이제 타입 오류 없음
function authMiddleware(req: Request, res: Response, next: Function) {
req.user = verifyToken(req.headers.authorization); // ✅
next();
}
기본 문법
외부 모듈 보강
// 반드시 import가 있어야 모듈 파일로 인식됨
import type { SomeType } from 'some-library';
declare module 'some-library' {
// 기존 인터페이스 확장
interface ExistingInterface {
newProperty: string;
}
// 새 함수 추가
function newFunction(arg: string): void;
}
인터페이스 병합 (Declaration Merging)
TypeScript의 인터페이스는 같은 이름으로 여러 번 선언해도 자동으로 병합됩니다.
// 라이브러리의 원본 정의
interface Window {
location: Location;
history: History;
}
// 우리가 추가하는 정의
interface Window {
myPlugin: MyPlugin;
analytics: Analytics;
}
// TypeScript는 두 선언을 합쳐서 처리
// Window = { location, history, myPlugin, analytics }
실전 예제 모음
예제 1: Express Request 확장
// src/types/express-ext.d.ts
import type { User } from '../models/user';
import type { Logger } from '../utils/logger';
declare module 'express-serve-static-core' {
interface Request {
user?: User;
logger: Logger;
startTime: number;
requestId: string;
}
}
// src/middleware/auth.ts
import { Request, Response, NextFunction } from 'express';
export function authMiddleware(req: Request, res: Response, next: NextFunction) {
try {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) throw new Error('No token');
req.user = verifyJwt(token); // ✅ 타입 안전
req.requestId = generateId(); // ✅
next();
} catch {
res.status(401).json({ error: 'Unauthorized' });
}
}
예제 2: 전역 Window 객체 확장
// src/types/global.d.ts
interface Window {
// Google Analytics
gtag: (command: string, id: string, params?: Record<string, unknown>) => void;
// 앱 설정
__APP_CONFIG__: {
apiUrl: string;
version: string;
featureFlags: Record<string, boolean>;
};
// Stripe
Stripe?: (key: string) => StripeInstance;
}
// 사용 시
window.gtag('event', 'page_view', { page_path: '/home' }); // ✅
const config = window.__APP_CONFIG__;
console.log(config.version); // ✅
예제 3: 라이브러리 인터페이스 확장
// Fastify 플러그인 타입 확장
import 'fastify';
declare module 'fastify' {
interface FastifyInstance {
db: DatabaseClient;
redis: RedisClient;
jwt: JwtHelper;
}
interface FastifyRequest {
user?: AuthUser;
session: SessionData;
}
}
// Fastify 플러그인 구현
import fp from 'fastify-plugin';
export default fp(async (fastify) => {
const db = await createDatabaseClient();
fastify.decorate('db', db); // ✅ fastify.db 타입 안전
});
// 라우트에서 사용
fastify.get('/users', async (request, reply) => {
const users = await request.server.db.query('SELECT * FROM users'); // ✅
return users;
});
예제 4: Vuex / Pinia 스토어 타입 확장
// Pinia 스토어 타입 확장
import 'pinia';
declare module 'pinia' {
export interface PiniaCustomProperties {
$router: Router;
$api: ApiClient;
}
export interface DefineStoreOptionsBase<S, Store> {
debounce?: Partial<Record<keyof StoreActions<Store>, number>>;
}
}
예제 5: process.env 타입 정의
// src/types/env.d.ts
declare namespace NodeJS {
interface ProcessEnv {
NODE_ENV: 'development' | 'production' | 'test';
DATABASE_URL: string;
JWT_SECRET: string;
PORT?: string;
API_KEY?: string;
}
}
// 사용 시 타입 안전
const dbUrl: string = process.env.DATABASE_URL; // ✅ string (undefined 아님)
const port = parseInt(process.env.PORT ?? '3000', 10); // ✅
// ❌ 잘못된 접근 즉시 감지
process.env.TYPO_KEY; // Property 'TYPO_KEY' does not exist
전역 타입 선언
모듈 파일이 아닌 스크립트 파일에서의 전역 선언입니다.
// src/types/global.d.ts (import/export가 없는 파일)
// 전역 타입 선언
type ID = string | number;
type Nullable<T> = T | null;
type Optional<T> = T | undefined;
// 전역 인터페이스
interface Pagination {
page: number;
limit: number;
total: number;
}
// 전역 함수
declare function log(message: string, level?: 'info' | 'warn' | 'error'): void;
// 어디서든 import 없이 사용 가능
const id: ID = '123'; // ✅
const page: Pagination = { page: 1, limit: 10, total: 100 }; // ✅
주의:
import나export가 있는 파일은 모듈로 취급됩니다. 전역 선언을 하려면declare global {}블록을 사용하세요.
// src/types/augment.d.ts (import가 있어도 전역 선언 가능)
import type { User } from './user';
// 전역 선언은 declare global 블록 안에
declare global {
interface Window {
currentUser: User;
}
type ID = string | number;
}
export {}; // 빈 export로 모듈 파일 처리
모듈 보강 주의사항
1. 올바른 모듈 이름 확인
// Express의 경우 실제 모듈 이름이 다름!
declare module 'express' { } // ❌ 작동 안 함
declare module 'express-serve-static-core' { } // ✅ 실제 인터페이스 위치
확인 방법:
# node_modules/@types/express/index.d.ts 열어서 확인
cat node_modules/@types/express/index.d.ts
2. 완전히 새로운 모듈 추가 불가
// 기존에 없는 인터페이스는 추가 불가 (보강만 가능)
declare module 'existing-lib' {
interface NewInterface { } // ✅ 새 인터페이스 추가 가능
function existingFn(): void; // ✅ 기존 함수 오버로드 추가 가능
}
3. tsconfig에 포함 확인
{
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./src/types"]
},
"include": ["src/**/*.ts", "src/types/**/*.d.ts"]
}
고수 팁
라이브러리별 올바른 보강 대상 찾기
# @types 패키지의 index.d.ts에서 인터페이스 이름 확인
grep -r "interface Request" node_modules/@types/express/
# → express-serve-static-core 모듈에 있음
grep -r "interface FastifyInstance" node_modules/@types/fastify/
타입 보강 파일 구조 추천
src/
└── types/
├── express.d.ts # Express 확장
├── env.d.ts # process.env 타입
├── global.d.ts # 전역 타입
└── window.d.ts # window 객체 확장
재사용 가능한 보강 패키지 만들기
팀 내에서 공통으로 사용하는 모듈 보강을 npm 패키지로 분리하면 여러 프로젝트에서 재사용할 수 있습니다.