18.5 JS → TS 마이그레이션 — 점진적 전환 전략
마이그레이션 전략 개요
단계적 마이그레이션 (권장):
1단계: @ts-check + JSDoc (코드 변경 없이 타입 체크)
2단계: allowJs + checkJs (JS와 TS 혼용)
3단계: 파일별 .js → .ts 전환
4단계: strict 모드 활성화
5단계: 타입 선언 완성
한 번에 전환 (소규모 프로젝트):
모든 .js → .ts 변경 + any로 우선 타입 지정
→ 점진적으로 any 제거
1단계: @ts-check와 JSDoc
// @ts-check
// 파일 상단에 추가 → VS Code에서 타입 체크 활성화
/**
* @param {string} name
* @param {number} age
* @returns {{ name: string; age: number; createdAt: Date }}
*/
function createUser(name, age) {
return { name, age, createdAt: new Date() }
}
/**
* @typedef {Object} Product
* @property {string} id
* @property {string} name
* @property {number} price
* @property {boolean} [inStock]
*/
/**
* @param {Product[]} products
* @returns {Product[]}
*/
function filterInStock(products) {
return products.filter(p => p.inStock)
}
2단계: allowJs + checkJs 설정
// tsconfig.json
{
"compilerOptions": {
"allowJs": true, // JS 파일 허용
"checkJs": true, // JS 파일도 타입 체크
"strict": false, // 처음엔 strict 끄기
"noImplicitAny": false, // any 암묵적 허용
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"] // .js와 .ts 모두 포함
}
3단계: 파일별 전환
# 파일 확장자 변경 스크립트
find src -name "*.js" -not -path "*/node_modules/*" | \
xargs -I {} bash -c 'mv "$1" "${1%.js}.ts"' _ {}
# 또는 한 파일씩
mv src/utils/helpers.js src/utils/helpers.ts
// 전환 초기 — any로 빠르게 통과
// src/legacy/user.ts
export function processUser(user: any): any {
return {
...user,
displayName: `${user.firstName} ${user.lastName}`,
}
}
// 점진적 개선 — any 제거
export interface LegacyUser {
firstName: string
lastName: string
email: string
}
export interface ProcessedUser extends LegacyUser {
displayName: string
}
export function processUser(user: LegacyUser): ProcessedUser {
return {
...user,
displayName: `${user.firstName} ${user.lastName}`,
}
}
4단계: strict 모드 단계적 활성화
// tsconfig.json — 옵션별 단계적 활성화
{
"compilerOptions": {
// 가장 기본적인 것부터
"noImplicitAny": true, // 1단계: any 명시 강제
"strictNullChecks": true, // 2단계: null 체크
"strictFunctionTypes": true, // 3단계: 함수 타입 엄격
"strictPropertyInitialization": true, // 4단계: 프로퍼티 초기화
"noImplicitThis": true, // 5단계: this 타입
// "strict": true // 모두 합친 것 — 마지막에 활성화
}
}
레거시 패턴 TypeScript 변환
// 레거시 JS 패턴 → TypeScript 변환 예시
// 1. 클래스 없이 프로토타입 패턴
// 이전 (JS)
function Animal(name) {
this.name = name
}
Animal.prototype.speak = function() {
return `${this.name} makes a noise.`
}
// 이후 (TS)
class Animal {
constructor(public readonly name: string) {}
speak(): string {
return `${this.name} makes a noise.`
}
}
// 2. arguments 객체
// 이전 (JS)
function sum() {
return Array.from(arguments).reduce((a, b) => a + b, 0)
}
// 이후 (TS)
function sum(...numbers: number[]): number {
return numbers.reduce((a, b) => a + b, 0)
}
// 3. 동적 객체 접근
// 이전 (JS)
function getValue(obj, key) {
return obj[key]
}
// 이후 (TS)
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
// 4. 콜백 기반 → Promise/async
// 이전 (JS)
function readFileCallback(path, callback) {
fs.readFile(path, 'utf-8', (err, data) => {
if (err) return callback(err)
callback(null, data)
})
}
// 이후 (TS)
async function readFileAsync(path: string): Promise<string> {
return fs.promises.readFile(path, 'utf-8')
}
서드파티 라이브러리 타입
# @types 패키지 설치
npm install --save-dev @types/node @types/express @types/lodash
# 타입 없는 라이브러리 처리
# tsconfig.json
{
"compilerOptions": {
"typeRoots": ["./node_modules/@types", "./types"]
}
}
// types/legacy-lib.d.ts — 타입 없는 라이브러리 선언
declare module 'legacy-library' {
export function doSomething(value: string): Promise<void>
export function compute(a: number, b: number): number
export const VERSION: string
}
// 간단한 경우 — module 선언만
declare module 'untyped-module' // any 타입으로 처리
고수 팁
마이그레이션 진행률 측정
# any 개수 측정 (줄어들수록 마이그레이션 진행)
grep -r ": any" src --include="*.ts" | wc -l
# ts-migrate 자동 마이그레이션 도구
npm install -g @ts-migrate/ts-migrate
ts-migrate-full /path/to/project
점진적 엄격화 자동화
// .eslintrc.json에 규칙 추가
{
"rules": {
"@typescript-eslint/no-explicit-any": "warn", // 경고 → 나중에 error로
"@typescript-eslint/no-unsafe-assignment": "warn",
"@typescript-eslint/no-unsafe-call": "warn"
}
}