레거시 코드 마이그레이션 6개월 - JavaScript에서 TypeScript로 옮기며 배운 것들

January 05, 2026

레거시 코드 마이그레이션 6개월

10만 줄의 JavaScript 코드를 TypeScript로 옮기는 작업을 시작했을 때, 나는 이게 이렇게 어려울 줄 몰랐다. “타입만 추가하면 되지 않을까?”라는 순진한 생각으로 시작했지만, 현실은 달랐다. 6개월 동안의 고생과 배움, 그리고 결국 성공한 이야기를 기록한다.

시작: TypeScript 도입 결정

왜 TypeScript를 선택했나

2025년 7월, 우리 프로젝트는 10만 줄이 넘는 JavaScript 코드로 구성되어 있었다. 5년간 쌓인 레거시 코드였다. 버그가 자주 발생했고, 특히 타입 관련 에러가 많았다.

“TypeScript를 도입하면 타입 안정성이 생기고, 버그가 줄어들 거야.” 이렇게 생각하며 TypeScript 도입을 제안했다.

팀 내부의 반대

하지만 팀원들의 반응은 냉랭했다.

“10만 줄을 다 바꿔야 해?” “시간이 너무 오래 걸릴 것 같은데.” “지금도 바쁜데 마이그레이션까지?”

특히 시니어 개발자 한 분이 강하게 반대했다. “레거시 코드를 건드리면 버그가 더 생길 수 있어. 위험하다.”

하지만 나는 포기하지 않았다. 점진적 마이그레이션 전략을 제안했다. 한 번에 다 바꾸는 게 아니라, 새로 작성하는 코드부터 TypeScript로 작성하고, 기존 코드는 점진적으로 변환하자고.

첫 번째 도전: 점진적 마이그레이션 전략

allowJs 옵션으로 시작

처음에는 tsconfig.jsonallowJs: true를 설정했다. 이렇게 하면 JavaScript 파일과 TypeScript 파일이 공존할 수 있었다.

{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": false,
    "noEmit": true
  }
}

새로 작성하는 파일부터 .ts 확장자로 작성하기 시작했다. 처음 한 달은 순조로웠다. 새로운 기능을 TypeScript로 작성하면서 점점 타입의 이점을 느꼈다.

첫 번째 문제: 타입 정의의 부재

하지만 문제가 생겼다. 기존 JavaScript 코드를 사용할 때 타입이 없어서 any를 남발하게 되었다.

// 기존 JavaScript 함수 사용
import { getUserData } from './legacy.js'; // 타입이 없음

const user = getUserData(userId); // any 타입
user.name; // 타입 체크가 안 됨

이렇게 되면 TypeScript의 장점을 제대로 활용할 수 없었다. 기존 코드에 타입 정의를 추가해야 했다.

두 번째 도전: 기존 코드에 타입 추가하기

JSDoc으로 시작

처음에는 JSDoc 주석으로 타입을 추가했다. 기존 코드를 수정하지 않고도 타입 정보를 제공할 수 있었다.

/**
 * @param {string} userId
 * @returns {Promise<{id: string, name: string, email: string}>}
 */
async function getUserData(userId) {
  // ...
}

하지만 이 방법도 한계가 있었다. JSDoc은 타입 체크가 약하고, 복잡한 타입을 표현하기 어려웠다.

.d.ts 파일 생성

그래서 타입 정의 파일(.d.ts)을 만들기 시작했다. 기존 JavaScript 파일에 대응하는 타입 정의 파일을 작성했다.

// legacy.d.ts
export declare function getUserData(userId: string): Promise<{
  id: string;
  name: string;
  email: string;
}>;

하지만 10만 줄의 코드에 타입 정의를 모두 작성하는 것은 불가능했다. 우선순위를 정해야 했다.

세 번째 도전: 우선순위 정하기

자주 사용되는 코드부터

우선순위를 정했다:

  1. 공통 유틸리티 함수
  2. API 호출 함수
  3. 데이터 모델
  4. 비즈니스 로직

자주 사용되는 코드부터 타입을 추가하기 시작했다. 하지만 이것도 생각보다 시간이 오래 걸렸다.

예상치 못한 문제들

문제 1: 외부 라이브러리 타입 정의 부재

일부 오래된 라이브러리는 타입 정의가 없었다. @types/ 패키지도 없었다. 직접 타입 정의를 작성해야 했다.

문제 2: 동적 타입의 함정

JavaScript의 유연함이 문제가 되었다. 객체에 동적으로 프로퍼티를 추가하는 패턴이 많았는데, 이를 TypeScript로 표현하기 어려웠다.

// 기존 JavaScript 코드
const user = {};
user.name = 'John';
user.age = 30;

이런 패턴을 TypeScript로 바꾸려면 인터페이스를 정의해야 했다.

문제 3: any 타입의 남용

처음에는 빠르게 진행하기 위해 any를 많이 사용했다. 하지만 나중에 문제가 되었다. any를 사용하면 TypeScript의 장점을 잃게 된다.

네 번째 도전: 팀원들의 저항

타입 정의 작성의 부담

3개월이 지나면서 팀원들의 불만이 커졌다. 타입 정의를 작성하는 것이 부담이 되었다.

“이전에는 그냥 코드만 작성하면 됐는데, 이제는 타입도 써야 해.” “타입 에러 때문에 개발 속도가 느려졌어.”

특히 주니어 개발자들이 힘들어했다. TypeScript 문법을 익히는 것도 어려웠고, 타입을 제대로 정의하는 것도 어려웠다.

내 고민

나도 고민이 많았다. 분명히 좋은 선택이라고 생각했는데, 팀원들의 불만을 보면서 “내가 잘못 선택한 건가?”라는 생각이 들었다.

하지만 이미 3개월을 투자했고, 되돌리기엔 너무 많은 작업이 필요했다. 그래도 계속 밀어붙였지만, 상황은 나아지지 않았다.

전환점: 타입의 이점을 보기 시작하다

버그 감소

4개월이 지나면서 타입의 이점이 보이기 시작했다. 타입 체크로 인해 런타임 에러가 줄어들었다.

이전:

  • 주간 버그 리포트: 평균 15개
  • 타입 관련 에러: 평균 5개

4개월 후:

  • 주간 버그 리포트: 평균 10개
  • 타입 관련 에러: 평균 1개

특히 null이나 undefined 관련 에러가 크게 줄었다. TypeScript가 이런 문제를 컴파일 타임에 잡아줬다.

개발 경험 개선

IDE의 자동 완성도 훨씬 좋아졌다. 타입 정보가 있어서 함수의 파라미터와 반환값을 정확히 알 수 있었다.

// 타입이 있을 때
const user = await getUserData(userId);
user. // 자동 완성으로 name, email 등이 제안됨

리팩토링의 안정성

리팩토링할 때도 자신감이 생겼다. 타입 체크가 있어서 실수를 미리 잡을 수 있었다.

다섯 번째 도전: 완전한 마이그레이션

마지막 JavaScript 파일들

5개월이 지나면서 대부분의 코드가 TypeScript로 변환되었다. 하지만 아직도 약 2만 줄의 JavaScript 코드가 남아있었다.

이 코드들은 가장 복잡하고 위험한 부분이었다. 건드리기 두려운 레거시 코드였다.

strict 모드 활성화

대부분의 코드가 TypeScript로 변환되자, strict 모드를 활성화했다. 이제 타입 체크가 더 엄격해졌다.

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true
  }
}

당연히 에러가 많이 발생했다. 하지만 이 에러들을 하나씩 수정하면서 코드 품질이 향상되었다.

여섯 번째 달: 마무리와 성찰

마지막 JavaScript 파일 변환

6개월째, 마지막 JavaScript 파일들을 TypeScript로 변환했다. 가장 어려운 부분이었다. 복잡한 비즈니스 로직과 레거시 패턴들이 얽혀있었다.

하지만 끝까지 포기하지 않았다. 하나씩 타입을 추가하고, 리팩토링하고, 테스트했다.

마이그레이션 완료

2025년 12월, 드디어 마이그레이션이 완료되었다. 10만 줄의 JavaScript 코드가 모두 TypeScript로 변환되었다.

결과:

  • 타입 커버리지: 95%
  • 버그 감소: 30%
  • 개발 속도: 초기에는 느려졌지만, 3개월 후에는 오히려 빨라짐
  • 코드 품질: 크게 향상

배운 점들

1. 점진적 마이그레이션이 중요하다

한 번에 모든 것을 바꾸려고 하지 말고, 점진적으로 진행하는 것이 중요하다. allowJs 옵션을 사용해서 JavaScript와 TypeScript가 공존할 수 있게 한 것이 핵심이었다.

2. 팀원들의 이해와 협력이 필수다

마이그레이션은 혼자서 할 수 있는 작업이 아니다. 팀원들의 이해와 협력이 필수다. 초기 반대가 있었지만, 타입의 이점을 보여주면서 점차 동의를 얻을 수 있었다.

3. 타입 정의 작성에 시간을 투자하라

타입 정의를 제대로 작성하는 것이 중요하다. 처음에는 빠르게 진행하기 위해 any를 많이 사용했지만, 나중에 문제가 되었다. 처음부터 제대로 작성하는 것이 장기적으로는 더 빠르다.

4. 실용적인 접근이 필요하다

완벽한 타입 정의를 만들려고 하지 말고, 실용적인 접근이 필요하다. 100% 타입 안전성을 추구하기보다는, 80%의 타입 안전성으로도 충분히 이점을 얻을 수 있다.

5. 인내심이 필요하다

마이그레이션은 시간이 오래 걸린다. 6개월이라는 시간이 걸렸지만, 그만한 가치가 있었다. 인내심을 가지고 꾸준히 진행하는 것이 중요하다.

현재 상태와 앞으로

TypeScript로의 완전한 전환

지금은 모든 새 코드를 TypeScript로 작성하고 있다. 팀원들도 TypeScript에 익숙해졌고, 타입의 이점을 체감하고 있다.

지속적인 개선

마이그레이션이 완료되었지만, 여전히 개선할 점이 많다. any 타입을 줄이고, 더 엄격한 타입을 사용하고, 타입 유틸리티를 활용하는 등 지속적으로 개선하고 있다.

다른 개발자들에게

TypeScript 마이그레이션을 고려한다면

레거시 코드를 TypeScript로 마이그레이션하려는 팀이 있다면, 다음을 고려해보길 바란다:

  1. 점진적 접근: 한 번에 다 바꾸지 말고 점진적으로 진행하라
  2. 팀원들의 이해: 타입의 이점을 보여주고 팀원들의 동의를 얻어라
  3. 우선순위 정하기: 자주 사용되는 코드부터 타입을 추가하라
  4. 인내심: 마이그레이션은 시간이 오래 걸린다. 인내심을 가져라
  5. 실용적 접근: 완벽을 추구하지 말고 실용적으로 접근하라

실패를 두려워하지 말라

우리의 경험이 다른 팀에게는 도움이 될 수 있다. 마이그레이션은 어렵지만, 그만한 가치가 있다. 타입 안정성, 버그 감소, 개발 경험 개선 등 많은 이점을 얻을 수 있다.

결론: 6개월의 여정

6개월 동안의 마이그레이션은 쉽지 않았다. 팀원들의 반대, 예상치 못한 문제들, 타입 정의 작성의 부담까지. 하지만 결국 성공했고, 그 과정에서 많은 것을 배웠다.

지금은 TypeScript로 모든 코드를 작성하고 있다. 타입의 이점을 매일 느끼고 있다. 버그가 줄어들고, 개발 경험이 좋아지고, 코드 품질이 향상되었다.

마이그레이션은 투자다. 시간과 노력을 투자하면, 그만한 가치를 얻을 수 있다. 다른 개발자들도 이 경험이 도움이 되기를 바란다.


Written by Jeon Byung Hun 개발을 즐기는 bottlehs - Engineer, MS, AI, FE, BE, OS, IOT, Blockchain, 설계, 테스트