Ch 01. 마이그레이션 전략 수립
TypeScript로 전환하겠다고 결심했습니다. 그런데 막상 프로젝트 폴더를 열어보면 막막합니다. .js 파일이 80개, node_modules는 제외하더라도 비즈니스 로직만 3만 줄. 어디서부터 손을 대야 할까요?
가장 먼저 해야 할 일은 코드를 건드리는 것이 아닙니다. 전략을 세우는 것입니다.
두 가지 길: 점진적 vs 일괄
마이그레이션 방식은 크게 두 가지입니다.
일괄 전환(Big Bang)은 모든 파일을 한꺼번에 .ts로 바꾸는 방식입니다. 결정이 깔끔하고, "마이그레이션 중간 상태"가 없어서 코드가 일관됩니다. 소규모 프로젝트나 실험적인 프로젝트라면 이 방법이 빠릅니다.
하지만 현실의 서비스 프로젝트에서 일괄 전환은 위험합니다. 수백 개의 파일을 동시에 수정하면 리뷰하기 어렵고, 빌드가 한동안 깨진 상태로 유지되며, 팀원 전체가 같은 시간에 "TypeScript 공부 + 업무 + 전환"을 동시에 해야 합니다.
점진적 전환(Incremental)은 JavaScript와 TypeScript가 공존하는 기간을 허용합니다. 파일 하나씩, 모듈 하나씩 전환합니다. 서비스는 계속 돌아가고, 리뷰는 작은 단위로 할 수 있으며, 팀원이 적응할 시간이 생깁니다.
TypeScript는 처음부터 점진적 전환을 지원하도록 설계되었습니다. allowJs 옵션이 그 증거입니다.
allowJs로 공존하기
tsconfig.json에 allowJs: true를 추가하면 TypeScript 컴파일러가 .js 파일도 처리합니다. .ts와 .js가 같은 프로젝트 안에서 서로를 import할 수 있게 됩니다.
// 수정: tsconfig.json{ "compilerOptions": { "target": "ES2020", "module": "commonjs", "outDir": "./dist", "rootDir": "./src", "allowJs": true, "checkJs": false, "strict": false, "esModuleInterop": true }, "include": ["src"]}
checkJs: false로 두면 .js 파일에 대해서는 타입 검사를 하지 않습니다. 기존 JavaScript 코드가 아무리 느슨하게 작성되어 있어도 에러가 나지 않습니다. .ts로 전환한 파일만 타입 검사를 받습니다.
나중에 .js 파일에도 타입 힌트를 추가하고 싶다면 checkJs: true로 바꿉니다. JSDoc 주석으로 타입을 선언하면 TypeScript가 읽어서 검사해줍니다.
// filename: src/utils/formatDate.js/** * @param {Date} date * @returns {string} */function formatDate(date) { return date.toISOString().split('T')[0];}module.exports = { formatDate };
checkJs: true 상태에서 위 함수에 string을 전달하면 에러가 납니다. JSDoc이 타입 문서이자 검사 기준이 되는 것입니다.
어떤 파일을 먼저 전환할까
모든 파일이 동일한 우선순위를 가지지 않습니다. 어디서 시작할지 판단하는 기준이 있습니다.
변경이 잦은 파일부터. 앞으로도 자주 수정될 파일에 타입을 추가하면 이후 작업이 편해집니다. 반대로 거의 건드리지 않는 레거시 코드는 나중에 해도 됩니다.
의존성이 적은 파일부터. 다른 파일을 많이 import하는 파일보다, 독립적으로 동작하는 유틸리티 함수나 헬퍼 모듈이 전환하기 쉽습니다. 의존 그래프의 잎(leaf) 노드부터 시작하는 것이 원칙입니다.
타입이 명확한 파일부터. 순수 함수, 데이터 변환 유틸리티, API 응답 파싱 함수처럼 입력과 출력이 명확한 코드는 타입 추가가 쉽습니다. any를 남발하지 않아도 되는 코드입니다.
버그가 발생하던 곳부터. TypeScript 도입의 가장 큰 이유가 버그 예방이라면, 실제로 문제가 많이 생기던 모듈을 우선순위에 올립니다.
우선순위 판단 매트릭스
실무에서 파일 목록을 보고 우선순위를 매길 때는 두 축으로 생각합니다.
전환 가치 (높음)
│
D │ A
│
───────┼───────── 전환 비용 (높음)
│
C │ B
│
전환 가치 (낮음)
- A (높은 가치, 높은 비용): 핵심 비즈니스 로직. 전환 필요하지만 신중하게.
- B (낮은 가치, 높은 비용): 복잡한 레거시 유틸. 마지막에.
- C (낮은 가치, 낮은 비용): 단순 상수, 설정 파일. 빠르게 처리.
- D (높은 가치, 낮은 비용): 독립 유틸, API 클라이언트. 가장 먼저.
tsconfig.json 마이그레이션 전용 설정
전환 초기에는 엄격한 설정을 피합니다. 먼저 컴파일이 통과하는 것을 목표로 삼고, 이후 점진적으로 조입니다.
// 수정: tsconfig.json{ "compilerOptions": { "target": "ES2020", "module": "commonjs", "outDir": "./dist", "rootDir": "./src", "allowJs": true, "checkJs": false, "strict": false, "noImplicitAny": false, "skipLibCheck": true, "esModuleInterop": true, "resolveJsonModule": true }, "include": ["src"], "exclude": ["node_modules", "dist"]}
skipLibCheck: true는 node_modules 안의 타입 선언 파일 오류를 무시합니다. 외부 라이브러리 타입 불일치로 인한 노이즈를 제거해줍니다.
팀 합의가 먼저다
기술적 준비보다 중요한 것이 있습니다. 팀이 마이그레이션에 동의해야 합니다.
TypeScript 경험이 없는 팀원이 있다면 학습 시간을 미리 확보해야 합니다. 마이그레이션 일정을 스프린트에 반영하지 않으면 기능 개발과 충돌합니다. 코드 리뷰 기준도 사전에 정해야 합니다. "이 PR은 TypeScript 전환 PR이므로 타입 에러가 남아 있어도 됩니다"라는 합의가 없으면 리뷰가 막힙니다.
전환은 한 사람이 혼자 밤새 해치우는 일이 아닙니다. 팀 전체가 방향을 공유하고 함께 걸어가는 과정입니다.
다음 챕터에서는 실제 파일을 열고 .js에서 .ts로 바꾸는 과정을 손으로 따라갑니다.