레거시 JavaScript 프로젝트에 TypeScript 점진적으로 도입하기

배경

팀 내에서 타입 관련 버그가 자주 발생하면서 TypeScript 도입을 논의하게 되었다. 하지만 2년간 작성된 JavaScript 코드를 한 번에 전환하는 것은 현실적으로 불가능했다. 점진적 마이그레이션 전략을 세우고 실행에 옮겼다.

설정

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

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "allowJs": true,
    "checkJs": false,
    "jsx": "react",
    "strict": false,
    "esModuleInterop": true
  }
}

처음부터 strict 모드를 켜면 마이그레이션이 너무 힘들어서 일단 꺼두었다. 나중에 점진적으로 켤 계획이다.

마이그레이션 순서

  1. 유틸리티 함수부터 시작
  2. 타입 정의가 명확한 모듈
  3. 새로 작성하는 코드는 무조건 TypeScript

처음에는 utils 디렉토리의 순수 함수들부터 .ts로 변환했다.

// before: utils.js
export function formatDate(date) {
  return date.toISOString().split('T')[0];
}

// after: utils.ts
export function formatDate(date: Date): string {
  return date.toISOString().split('T')[0];
}

마주친 문제들

외부 라이브러리 타입 정의 부족

사용 중인 일부 npm 패키지에 @types가 없었다. 일단 declare module로 임시 처리했다.

declare module 'legacy-library' {
  export function doSomething(param: any): any;
}

암묵적 any 타입

레거시 코드를 변환하다 보면 타입을 정확히 알 수 없는 경우가 많았다. 처음에는 any를 사용하고 주석으로 TODO를 남겨두었다.

빌드 시간 증가

TypeScript 컴파일이 추가되면서 빌드 시간이 약 30% 증가했다. ts-loader 대신 babel-loader@babel/preset-typescript를 사용하는 방식으로 변경해서 어느 정도 개선했다.

소감

아직 20% 정도만 변환했지만, 이미 효과를 체감하고 있다. IDE에서 자동완성이 잘 작동하고, 리팩토링할 때 실수가 줄었다. 점진적 마이그레이션이 정답이었다.

레거시 JavaScript 프로젝트에 TypeScript 점진적으로 도입하기