기존 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 모드를 켜면 기존 코드에서 수백 개의 에러가 발생한다. 일단 느슨하게 시작해서 점차 엄격하게 만드는 전략을 택했다.
마이그레이션 순서
- 유틸리티 함수부터 - 의존성이 적은 순수 함수들을 먼저
.ts로 변환했다. - 타입 정의 작성 -
@types패키지가 없는 써드파티 라이브러리는src/types디렉토리에.d.ts파일로 정의했다. - 미들웨어 레이어 - Request, Response 타입을 명시하면서 에러 핸들링 로직이 명확해졌다.
- 라우터와 컨트롤러 - 점진적으로 확장하며 타입 커버리지를 높였다.
실제 적용 예시
기존 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로 작성하기로 했다. 점진적 마이그레이션 전략이 운영 중인 서비스에는 적합했다고 생각한다.