TypeScript 마이그레이션 중 any 타입과의 전쟁

문제 상황

회사 메인 프로젝트를 TypeScript로 전환하기로 결정했다. 3년간 운영된 React 프로젝트였고, 코드베이스가 상당했다. 일단 .js 파일을 .tsx로 바꾸고 any를 붙여가며 빌드부터 통과시켰는데, 이게 문제의 시작이었다.

// 초기 마이그레이션 코드
function fetchUserData(id: any): any {
  return api.get(`/users/${id}`).then((res: any) => res.data);
}

TypeScript를 도입한 의미가 없었다. any 투성이라 런타임 에러는 여전했다.

해결 과정

1. tsconfig.json 설정 조정

처음엔 strict: false로 시작했다가, 단계적으로 옵션을 켜기로 했다.

{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": false,
    "strictFunctionTypes": false
  }
}

2. API 응답 타입 정의

가장 먼저 외부 API 응답부터 타입을 정의했다. Swagger 문서를 참고해서 인터페이스를 작성했다.

interface User {
  id: number;
  name: string;
  email: string;
  createdAt: string;
}

function fetchUserData(id: number): Promise<User> {
  return api.get(`/users/${id}`).then(res => res.data);
}

3. Utility Types 활용

중복 타입 정의를 줄이기 위해 TypeScript 내장 유틸리티 타입을 적극 활용했다.

type UserFormData = Pick<User, 'name' | 'email'>;
type PartialUser = Partial<User>;

교훈

  • 마이그레이션은 빌드 통과가 목표가 아니라 타입 안정성 확보가 목표다
  • strict 모드를 처음부터 켜는 게 장기적으론 이득이다
  • API 경계부터 타입을 정의하면 내부로 전파하기 쉽다

여전히 일부 복잡한 고차 컴포넌트에는 any가 남아있지만, 핵심 도메인 로직은 타입 커버리지가 80%를 넘었다. 다음 분기엔 strictNullChecks를 켤 예정이다.

TypeScript 마이그레이션 중 any 타입과의 전쟁