기존 Express 프로젝트에 TypeScript 점진적으로 도입하기

배경

2년 가까이 운영해온 Express 기반 API 서버가 있었다. 코드베이스가 커지면서 타입 관련 버그가 잦아졌고, 리팩토링 시 사이드 이펙트를 예측하기 어려워졌다. 팀 내에서 TypeScript 도입 논의가 있었고, 전체를 한 번에 바꾸기보다 점진적으로 적용하기로 했다.

초기 설정

먼저 tsconfig.json을 추가하고 allowJs: true 옵션을 활성화했다. 이렇게 하면 기존 .js 파일과 새로운 .ts 파일이 공존할 수 있다.

{
  "compilerOptions": {
    "target": "es2017",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "allowJs": true,
    "checkJs": false,
    "strict": false,
    "esModuleInterop": true
  }
}

처음부터 strict 모드를 켜면 기존 코드에서 수백 개의 에러가 발생한다. 일단 느슨하게 시작해서 점차 엄격하게 만드는 전략을 택했다.

마이그레이션 순서

  1. 유틸리티 함수부터 - 의존성이 적은 순수 함수들을 먼저 .ts로 변환했다.
  2. 타입 정의 작성 - @types 패키지가 없는 써드파티 라이브러리는 src/types 디렉토리에 .d.ts 파일로 정의했다.
  3. 미들웨어 레이어 - Request, Response 타입을 명시하면서 에러 핸들링 로직이 명확해졌다.
  4. 라우터와 컨트롤러 - 점진적으로 확장하며 타입 커버리지를 높였다.

실제 적용 예시

기존 JavaScript 미들웨어:

const authMiddleware = (req, res, next) => {
  const token = req.headers.authorization;
  if (!token) {
    return res.status(401).json({ error: 'Unauthorized' });
  }
  // 토큰 검증 로직
  next();
};

TypeScript로 변환:

import { Request, Response, NextFunction } from 'express';

interface AuthRequest extends Request {
  user?: { id: string; email: string };
}

const authMiddleware = (
  req: AuthRequest,
  res: Response,
  next: NextFunction
): void => {
  const token = req.headers.authorization;
  if (!token) {
    res.status(401).json({ error: 'Unauthorized' });
    return;
  }
  // 토큰 검증 후 req.user 할당
  next();
};

얻은 것

  • IDE 자동완성이 정확해지면서 개발 속도가 빨라졌다
  • API 응답 구조를 인터페이스로 정의하니 프론트엔드 팀과 소통이 명확해졌다
  • 리팩토링 시 컴파일 에러로 미리 문제를 발견할 수 있었다

아쉬운 점

  • 빌드 과정이 추가되어 배포 파이프라인 수정이 필요했다
  • 팀원들의 TypeScript 학습 시간이 필요했다
  • 일부 레거시 코드는 여전히 JavaScript로 남아있어 타입 안정성이 완벽하지 않다

당분간은 JavaScript와 TypeScript가 공존하겠지만, 새로운 기능은 모두 TypeScript로 작성하기로 했다. 점진적 마이그레이션 전략이 운영 중인 서비스에는 적합했다고 생각한다.

기존 Express 프로젝트에 TypeScript 점진적으로 도입하기