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

배경

팀 내 JavaScript 코드베이스가 2만 라인을 넘어가면서 런타임 에러가 잦아졌다. 특히 API 응답 구조가 바뀌거나 함수 인자를 잘못 전달하는 경우가 많았다. TypeScript 도입을 검토했지만, 전체를 한 번에 전환하기엔 일정상 무리였다.

마이그레이션 전략

tsconfig.json에서 allowJs: truecheckJs: false로 시작했다. JS와 TS 파일을 혼용할 수 있게 하되, 기존 JS는 체크하지 않는 방식이다.

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "lib": ["es2017", "dom"],
    "allowJs": true,
    "checkJs": false,
    "jsx": "react",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true
  }
}

우선순위는 다음과 같이 정했다:

  1. 유틸 함수와 공통 모듈
  2. API 클라이언트와 타입 정의
  3. 비즈니스 로직이 복잡한 컴포넌트
  4. 나머지 컴포넌트

실제 적용 사례

API 응답 타입을 먼저 정의했다.

// types/api.ts
export interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user';
}

export interface ApiResponse<T> {
  data: T;
  status: number;
  message?: string;
}

API 클라이언트를 TS로 전환하면서 타입 안정성을 확보했다.

// api/user.ts
import { User, ApiResponse } from '../types/api';

export async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  const json: ApiResponse<User> = await response.json();
  return json.data;
}

마주친 문제들

외부 라이브러리 타입 정의가 없는 경우가 많았다. @types/ 패키지를 찾거나, 없으면 직접 .d.ts 파일을 작성해야 했다.

// types/custom.d.ts
declare module 'legacy-library' {
  export function doSomething(value: string): void;
}

JS 파일에서 TS 파일을 import할 때 타입 체크가 안 되는 점도 아쉬웠다. 결국 중요 파일부터 먼저 변환하는 수밖에 없었다.

결과

3개월간 약 40%를 TS로 전환했다. 타입 에러를 컴파일 타임에 잡으면서 프로덕션 버그가 체감상 30% 정도 줄었다. IDE 자동완성도 정확해져서 개발 속도도 빨라졌다.

완전한 전환까지는 시간이 더 걸리겠지만, 점진적 접근이 현실적인 선택이었다.