TypeScript 점진적 도입 — JS 프로젝트에서 TS로
기존 JavaScript 코드베이스에 TypeScript를 점진적으로 도입하는 전략, allowJs·checkJs 설정, JSDoc 타입 주석, ts-migrate 자동화, CI 통합까지 완전히 정리합니다.
지난 글에서 tsconfig.json의 주요 옵션을 정리했다. 이번 글에서는 기존 JavaScript 프로젝트에 TypeScript를 점진적으로 도입하는 전략을 살펴본다. 처음부터 완전한 타입 안전성을 목표로 하면 방대한 코드베이스에서 팀 전체의 생산성이 일시에 떨어질 수 있다. 단계적 접근이 훨씬 현실적이다.
점진적 도입의 원칙
TypeScript 팀이 권장하는 마이그레이션의 원칙은 세 가지다.
- 빌드를 깨지 않는다 — 각 단계에서 기존 기능이 그대로 동작해야 한다
- 작은 단위로 자주 — 파일 단위, 모듈 단위로 전환
- 엄격함을 점진적으로 —
strict: false에서 시작해 단계적으로 높인다
Phase 1 — JS 그대로, 타입 검사만 추가
.js 파일을 변환하지 않고 타입 검사의 혜택을 받을 수 있다.
{
"compilerOptions": {
"allowJs": true, // JS 파일 포함
"checkJs": true, // JS 파일도 타입 검사
"strict": false, // 처음엔 느슨하게
"noEmit": true // 출력 없이 검사만
},
"include": ["src"]
}
checkJs: true만으로도 명백한 타입 오류를 잡아낸다. IDE 자동 완성 품질도 크게 향상된다.
JSDoc으로 JS 파일 타입 지정
.js 파일을 변환하지 않고 JSDoc 주석으로 타입 정보를 추가할 수 있다.
// @ts-check
/** @param {string} name @returns {string} */
function greet(name) {
return `Hello, ${name}!`;
}
/** @type {import('./types').UserConfig} */
const config = loadConfig();
/**
* @template T
* @param {T[]} arr
* @param {(item: T) => boolean} predicate
* @returns {T[]}
*/
function filter(arr, predicate) {
return arr.filter(predicate);
}
JSDoc 방식은 외부 라이브러리 타입(import('./types'))도 참조할 수 있어, 전환 기간에 .ts 파일과 .js 파일이 공존하는 환경에서 매우 유용하다.
Phase 2 — 새 파일부터 .ts 작성
새로 추가하는 파일은 모두 .ts로 작성하고, 기존 파일은 점진적으로 전환한다.
// 기존 JS 모듈을 가져올 때 타입 선언 파일 추가
// legacy-module.d.ts
declare module "./legacy-module" {
export function doSomething(input: string): number;
}
전환 우선순위는 자주 변경되는 파일 → 여러 곳에서 가져오는 공통 유틸 → 비즈니스 로직 핵심 모듈 순으로 잡는다.
자동 변환 도구
# ts-migrate: JS → TS 변환 + @ts-ignore 자동 삽입
npx ts-migrate migrate ./src
# 이후 @ts-ignore를 하나씩 제거하며 타입 추가
grep -r "@ts-ignore" src/ | wc -l # 남은 수 추적
ts-migrate는 완벽한 타입을 만들지 않고, 우선 컴파일이 통과하는 상태를 만든다. 이후 @ts-ignore와 any를 하나씩 제거하며 품질을 높인다.
오류 억제 주석
// @ts-ignore: 임시 억제 (이유를 반드시 설명)
// @ts-ignore: TODO 레거시 API 제거 후 삭제 예정
legacyApi.call(wrongType);
// @ts-expect-error: 오류가 없어지면 경고 (권장)
// @ts-expect-error: 라이브러리 타입 오류, v3에서 수정 예정
someLib.undeclaredMethod();
// @ts-nocheck: 파일 전체 무시 (최후 수단)
@ts-expect-error는 오류가 사라지면 TS 컴파일러가 경고하므로, 임시 억제 후 잊어버리는 것을 방지한다.
Phase 3 — strict 완전 적용
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"allowJs": false
}
}
strict: true로 전환할 때 발생하는 오류 유형별 처리 방법이다.
| 오류 유형 | 처리 방법 |
|---|---|
암묵적 any | 타입 명시 또는 제네릭 추가 |
null 가능성 | 타입 가드 또는 ! 단언 (검증 후) |
| 함수 반환 타입 누락 | 명시적 반환 타입 추가 |
| 클래스 속성 미초기화 | 생성자 초기화 또는 declare |
CI 통합
# package.json
"type-check": "tsc --noEmit"
# GitHub Actions
- name: Type check
run: npm run type-check
타입 검사를 CI에 포함하면 오류가 main 브랜치에 병합되는 것을 방지한다. --noEmit으로 파일을 생성하지 않고 검사만 실행한다.
팀 도입 시 고려사항
- 점진적 strict:
tsconfig.strict-pending.json에 strict 옵션을 넣고, 준비된 파일부터 적용 - ESLint와 협력:
@typescript-eslint/no-explicit-any규칙으로any추가를 제한 - 타입 커버리지 추적:
npx type-coverage도구로 타입이 지정된 비율을 측정 - 새 팀원 온보딩: TypeScript 완전 전환 후 JS 경험만 있는 개발자도 빠르게 적응
TypeScript 도입은 한번에 완성하는 것이 아니라 지속적으로 품질을 높이는 과정이다. 다음 글에서는 빌드 도구(Babel, SWC, webpack, Vite)를 깊이 살펴본다.
지난 글: tsconfig 완전 정복 — 컴파일러 옵션 가이드
읽어주셔서 감사합니다. 😊