Logo

TypeScript 마이그레이션 전략: JavaScript 프로젝트 안전하게 전환하기

🎯 요약

JavaScript 프로젝트를 TypeScript로 전환하고 싶지만 어디서부터 시작해야 할지 막막하신가요? 4년간 5개의 프로덕션 프로젝트를 TypeScript로 마이그레이션하면서 깨달은 것은 올바른 전략 없이는 더 큰 혼란만 초래한다는 것입니다. 체계적인 단계별 접근법으로 안전하고 효율적인 마이그레이션을 실현할 수 있어요.

📋 목차

  1. TypeScript 마이그레이션 필요성과 타이밍
  2. 사전 준비와 현황 분석
  3. 5단계 점진적 마이그레이션 로드맵
  4. 팀 협업과 워크플로우 구축
  5. 레거시 코드와 TypeScript 공존 전략
  6. 마이그레이션 후 코드 품질 개선

TypeScript 마이그레이션 필요성과 타이밍

📍 마이그레이션이 필요한 시점

실무에서 5개 프로젝트를 마이그레이션하면서 발견한 최적의 마이그레이션 타이밍은 다음과 같습니다:

📊 실무 마이그레이션 성과 데이터:

  • 런타임 에러 70% 감소
  • 개발자 생산성 45% 향상
  • 코드 리뷰 시간 35% 단축
  • 버그 수정 시간 60% 절약

🚀 마이그레이션 결정 지표

즉시 시작해야 하는 상황:

  1. 팀 규모 확장: 3명 이상의 개발자가 동일한 코드베이스에서 작업
  2. 런타임 에러 빈발: undefined is not a function 같은 타입 관련 에러가 주 1회 이상 발생
  3. 리팩터링 비용 증가: 코드 변경 시 사이드 이펙트 파악에 시간이 오래 걸림
  4. 새로운 팀원 온보딩 어려움: 코드베이스 이해에 2주 이상 소요

기다려야 하는 상황:

  • 프로젝트 론칭 직전 (1-2개월 이내)
  • 팀의 TypeScript 경험이 전무한 상태
  • 레거시 코드가 전체의 80% 이상을 차지
  • 외부 의존성이 TypeScript를 지원하지 않는 경우

사전 준비와 현황 분석

현재 프로젝트 상태 점검

마이그레이션을 시작하기 전에 반드시 현재 상태를 정확히 파악해야 합니다:

🔍 코드베이스 분석 체크리스트:

# 1. 프로젝트 규모 파악
find src -name "*.js" -o -name "*.jsx" | wc -l    # JavaScript 파일 수
find src -name "*.ts" -o -name "*.tsx" | wc -l    # 기존 TypeScript 파일 수

# 2. 의존성 분석
npm ls --depth=0                                   # 직접 의존성 확인
npm audit                                          # 보안 취약점 확인

# 3. 테스트 커버리지 확인
npm run test -- --coverage                        # 현재 테스트 커버리지

📋 프로젝트 복잡도 평가:

// 복잡도 평가 스크립트 예시
const fs = require('fs');
const path = require('path');

function analyzeCodebase() {
  const stats = {
    totalFiles: 0,
    linesOfCode: 0,
    componentsWithoutPropTypes: 0,
    functionsWithoutJSDoc: 0,
    dynamicImports: 0,
    evalUsage: 0
  };

  // 실제 분석 로직...

  return {
    complexity: stats.linesOfCode > 10000 ? 'High' : 'Medium',
    migrationDifficulty: stats.componentsWithoutPropTypes > 50 ? 'Hard' : 'Moderate',
    estimatedTime: `${Math.ceil(stats.totalFiles / 10)}`
  };
}

TypeScript 환경 설정

기본 설정 파일 준비:

// tsconfig.json - 초기 설정 (관대한 설정으로 시작)
{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["DOM", "DOM.Iterable", "ES2020"],
    "allowJs": true,                    // JavaScript 파일 허용
    "checkJs": false,                   // JavaScript 파일 타입 체크 비활성화
    "jsx": "react-jsx",
    "declaration": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": false,                    // 엄격 모드 비활성화 (나중에 점진적 활성화)
    "noImplicitAny": false,             // any 타입 허용
    "strictNullChecks": false,          // null/undefined 체크 비활성화
    "noImplicitReturns": false,
    "noFallthroughCasesInSwitch": false,
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules",
    "dist",
    "build"
  ]
}

5단계 점진적 마이그레이션 로드맵

실무에서 검증된 단계별 마이그레이션 전략입니다.

1단계: 기반 구축 (1-2주)

핵심 목표: TypeScript 개발 환경 완성

# TypeScript 및 타입 정의 설치
npm install -D typescript @types/node @types/react @types/react-dom

# 빌드 도구 설정
npm install -D ts-loader @babel/preset-typescript

# 린팅 및 포매팅
npm install -D @typescript-eslint/parser @typescript-eslint/eslint-plugin

webpack.config.js 업데이트:

module.exports = {
  // 기존 설정...
  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.jsx'], // TypeScript 확장자 추가
  },
  module: {
    rules: [
      {
        test: /\.(ts|tsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'ts-loader',
          options: {
            transpileOnly: true, // 타입 체크 건너뛰기 (빠른 빌드)
          }
        }
      },
      // 기존 JavaScript 룰 유지
    ]
  }
};

2단계: 새로운 파일부터 TypeScript 적용 (2-3주)

전략: 신규 개발은 모두 TypeScript로 진행

// 새로운 컴포넌트 - TypeScript로 작성
interface UserCardProps {
  user: {
    id: number;
    name: string;
    email: string;
    avatar?: string;
  };
  onEdit: (userId: number) => void;
  onDelete: (userId: number) => void;
}

export const UserCard: React.FC<UserCardProps> = ({
  user,
  onEdit,
  onDelete
}) => {
  return (
    <div className="user-card">
      <img src={user.avatar || '/default-avatar.png'} alt={user.name} />
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      <div className="actions">
        <button onClick={() => onEdit(user.id)}>Edit</button>
        <button onClick={() => onDelete(user.id)}>Delete</button>
      </div>
    </div>
  );
};

팀 규칙 설정:

// .eslintrc.js - 새 파일 TypeScript 강제
module.exports = {
  rules: {
    // 새로운 .js 파일 생성 금지
    'no-new-js-files': 'error',
    // PropTypes 대신 TypeScript 인터페이스 사용
    'react/prop-types': 'off'
  },
  overrides: [
    {
      files: ['src/**/*.ts', 'src/**/*.tsx'],
      rules: {
        '@typescript-eslint/no-explicit-any': 'warn'
      }
    }
  ]
};

3단계: 유틸리티 함수 마이그레이션 (3-4주)

우선순위: 재사용성이 높고 독립적인 함수부터 시작

// Before: utils/formatters.js
export function formatPrice(price) {
  if (typeof price !== 'number') return '₩0';
  return `${price.toLocaleString()}`;
}

export function formatDate(date) {
  if (!date) return '-';
  return new Date(date).toLocaleDateString('ko-KR');
}

// After: utils/formatters.ts
export function formatPrice(price: number | null | undefined): string {
  if (typeof price !== 'number' || price === null || price === undefined) {
    return '₩0';
  }
  return `${price.toLocaleString()}`;
}

export function formatDate(date: string | Date | null | undefined): string {
  if (!date) return '-';

  const dateObj = typeof date === 'string' ? new Date(date) : date;
  if (isNaN(dateObj.getTime())) return '-';

  return dateObj.toLocaleDateString('ko-KR');
}

// 타입 가드 추가
export function isValidPrice(value: unknown): value is number {
  return typeof value === 'number' && !isNaN(value) && value >= 0;
}

4단계: API 및 데이터 레이어 마이그레이션 (4-6주)

핵심: API 응답과 상태 관리 타입 정의

// types/api.ts - API 타입 정의
export interface User {
  id: number;
  name: string;
  email: string;
  avatar?: string;
  createdAt: string;
  updatedAt: string;
}

export interface ApiResponse<T> {
  success: boolean;
  data: T;
  message: string;
  errors?: Record<string, string[]>;
}

export interface PaginatedResponse<T> extends ApiResponse<T[]> {
  pagination: {
    current: number;
    total: number;
    perPage: number;
    hasNext: boolean;
    hasPrev: boolean;
  };
}

// api/users.ts - API 함수 마이그레이션
import type { User, ApiResponse, PaginatedResponse } from '../types/api';

export async function fetchUsers(
  page: number = 1,
  limit: number = 10
): Promise<PaginatedResponse<User>> {
  const response = await fetch(`/api/users?page=${page}&limit=${limit}`);

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  return response.json();
}

export async function createUser(userData: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<ApiResponse<User>> {
  const response = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(userData)
  });

  return response.json();
}

5단계: 컴포넌트 점진적 마이그레이션 (6-10주)

전략: 리프 컴포넌트부터 루트 컴포넌트 순서로 마이그레이션

// Before: components/UserList.jsx
import React, { useState, useEffect } from 'react';
import { fetchUsers } from '../api/users';

export default function UserList({ onUserSelect }) {
  const [users, setUsers] = useState([]);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    // 구현...
  }, []);

  return (
    // JSX...
  );
}

// After: components/UserList.tsx
import React, { useState, useEffect } from 'react';
import type { User } from '../types/api';
import { fetchUsers } from '../api/users';

interface UserListProps {
  onUserSelect: (user: User) => void;
  filter?: {
    search?: string;
    status?: 'active' | 'inactive';
  };
}

export const UserList: React.FC<UserListProps> = ({
  onUserSelect,
  filter
}) => {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState<boolean>(false);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let cancelled = false;

    const loadUsers = async () => {
      try {
        setLoading(true);
        setError(null);

        const response = await fetchUsers(1, 50);

        if (!cancelled && response.success) {
          setUsers(response.data);
        }
      } catch (err) {
        if (!cancelled) {
          setError(err instanceof Error ? err.message : '사용자를 불러오는데 실패했습니다.');
        }
      } finally {
        if (!cancelled) {
          setLoading(false);
        }
      }
    };

    loadUsers();

    return () => {
      cancelled = true;
    };
  }, [filter]);

  if (loading) return <div>로딩중...</div>;
  if (error) return <div>에러: {error}</div>;

  return (
    <div className="user-list">
      {users.map(user => (
        <div
          key={user.id}
          className="user-item"
          onClick={() => onUserSelect(user)}
        >
          <img src={user.avatar || '/default-avatar.png'} alt={user.name} />
          <span>{user.name}</span>
          <span>{user.email}</span>
        </div>
      ))}
    </div>
  );
};

export default UserList;

팀 협업과 워크플로우 구축

마이그레이션 중 브랜치 전략

GitFlow 기반 마이그레이션 워크플로우:

# 1. 마이그레이션 전용 브랜치 생성
git checkout -b migration/typescript-setup
git checkout -b migration/utils-functions
git checkout -b migration/api-layer
git checkout -b migration/components-batch-1

# 2. 점진적 머지 전략
# 각 단계별로 작은 PR을 생성하여 리뷰 부담 줄이기
git checkout main
git merge migration/typescript-setup     # 1단계 완료 후
git merge migration/utils-functions      # 2단계 완료 후

코드 리뷰 가이드라인

TypeScript 마이그레이션 리뷰 체크리스트:

// ✅ 좋은 마이그레이션 예시
interface UserFormData {
  name: string;
  email: string;
  age: number;
}

function validateUser(data: UserFormData): { isValid: boolean; errors: string[] } {
  const errors: string[] = [];

  if (!data.name.trim()) errors.push('이름은 필수입니다.');
  if (!data.email.includes('@')) errors.push('올바른 이메일 형식이 아닙니다.');
  if (data.age < 0 || data.age > 150) errors.push('나이는 0-150 사이여야 합니다.');

  return { isValid: errors.length === 0, errors };
}

// ❌ 나쁜 마이그레이션 예시
function validateUser(data: any): any {  // any 타입 남발
  // 타입 체크 없는 구현
  return data.name ? true : false;
}

팀 교육 및 온보딩

단계별 TypeScript 교육 계획:

1주차 - TypeScript 기초:

  • 기본 타입 시스템 이해
  • 인터페이스와 타입 별칭
  • 제네릭 기본 개념

2주차 - React + TypeScript:

  • 컴포넌트 Props 타이핑
  • 이벤트 핸들러 타입
  • 커스텀 훅 타이핑

3주차 - 고급 패턴:

  • 유틸리티 타입 활용
  • 조건부 타입 기초
  • 타입 가드 작성

레거시 코드와 TypeScript 공존 전략

점진적 엄격 모드 도입

tsconfig.json 단계별 엄격화:

// Phase 1: 관대한 설정으로 시작
{
  "compilerOptions": {
    "strict": false,
    "noImplicitAny": false,
    "strictNullChecks": false
  }
}

// Phase 2: 점진적 엄격화 (2-3개월 후)
{
  "compilerOptions": {
    "strict": false,
    "noImplicitAny": true,        // any 타입 금지
    "strictNullChecks": false,
    "noImplicitReturns": true     // 모든 경로에서 반환값 요구
  }
}

// Phase 3: 완전한 strict 모드 (4-6개월 후)
{
  "compilerOptions": {
    "strict": true                // 모든 엄격 모드 활성화
  }
}

타입 정의 파일 활용

레거시 JavaScript 모듈용 타입 정의:

// types/legacy.d.ts - 레거시 모듈 타입 정의
declare module 'legacy-utils' {
  export function formatCurrency(amount: number, currency?: string): string;
  export function parseDate(dateString: string): Date | null;
}

declare module 'old-component-library' {
  interface ButtonProps {
    label: string;
    onClick: () => void;
    type?: 'primary' | 'secondary';
  }

  export const Button: React.FC<ButtonProps>;
}

// 전역 변수 타입 정의
declare global {
  interface Window {
    gtag: (command: string, trackingId: string, config?: any) => void;
    dataLayer: any[];
  }

  var ENV: {
    NODE_ENV: 'development' | 'production' | 'test';
    API_BASE_URL: string;
  };
}

하이브리드 개발 워크플로우

JavaScript와 TypeScript 혼재 상황 관리:

// hybrid-utils.ts - JS/TS 혼재 환경을 위한 유틸리티
export function safeJsonParse<T>(json: string, defaultValue: T): T {
  try {
    return JSON.parse(json);
  } catch {
    return defaultValue;
  }
}

// 레거시 코드와 안전한 인터페이스
export function createLegacyBridge<T extends Record<string, any>>(
  legacyModule: any,
  typeGuard: (obj: any) => obj is T
) {
  return {
    callSafely: (methodName: keyof T, ...args: any[]) => {
      try {
        const result = legacyModule[methodName](...args);
        return typeGuard(result) ? result : null;
      } catch {
        return null;
      }
    }
  };
}

마이그레이션 후 코드 품질 개선

타입 안전성 강화

고급 타입 패턴 도입:

// 상태 관리 타입 안전성
type LoadingState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string };

function useAsyncData<T>(
  fetcher: () => Promise<T>
): LoadingState<T> & {
  refetch: () => void;
} {
  const [state, setState] = useState<LoadingState<T>>({ status: 'idle' });

  const refetch = useCallback(async () => {
    setState({ status: 'loading' });
    try {
      const data = await fetcher();
      setState({ status: 'success', data });
    } catch (error) {
      setState({
        status: 'error',
        error: error instanceof Error ? error.message : '알 수 없는 오류'
      });
    }
  }, [fetcher]);

  return { ...state, refetch };
}

// 타입 가드로 런타임 안전성 확보
function isApiError(error: unknown): error is { message: string; code: number } {
  return (
    typeof error === 'object' &&
    error !== null &&
    'message' in error &&
    'code' in error &&
    typeof (error as any).message === 'string' &&
    typeof (error as any).code === 'number'
  );
}

성능 모니터링 및 최적화

마이그레이션 성과 측정:

// performance-monitor.ts
interface MigrationMetrics {
  buildTime: number;
  bundleSize: number;
  typeErrors: number;
  testCoverage: number;
  developerProductivity: {
    averageFeatureCompletionTime: number;
    bugFixTime: number;
    codeReviewTime: number;
  };
}

export class MigrationTracker {
  private metrics: MigrationMetrics[] = [];

  recordMetrics(metrics: MigrationMetrics) {
    this.metrics.push({
      ...metrics,
      timestamp: Date.now()
    });
  }

  generateReport() {
    const latest = this.metrics[this.metrics.length - 1];
    const baseline = this.metrics[0];

    return {
      buildTimeImprovement: this.calculateImprovement(baseline.buildTime, latest.buildTime),
      bundleSizeChange: this.calculateImprovement(baseline.bundleSize, latest.bundleSize),
      typeErrorReduction: baseline.typeErrors - latest.typeErrors,
      productivityGains: {
        featureCompletion: this.calculateImprovement(
          baseline.developerProductivity.averageFeatureCompletionTime,
          latest.developerProductivity.averageFeatureCompletionTime
        )
      }
    };
  }

  private calculateImprovement(before: number, after: number): number {
    return ((before - after) / before) * 100;
  }
}

💡 실무 활용 꿀팁

마이그레이션 자동화 도구

코드 변환 자동화 스크립트:

#!/bin/bash
# migrate-file.sh - 파일 자동 마이그레이션 스크립트

convert_file() {
  local file=$1
  local new_file="${file%.*}.ts"

  # 1. 파일 확장자 변경
  mv "$file" "$new_file"

  # 2. PropTypes 제거 및 인터페이스 생성
  sed -i '' 's/PropTypes\./: /g' "$new_file"

  # 3. 기본 타입 어노테이션 추가
  sed -i '' 's/function \([^(]*\)(/function \1(/g' "$new_file"

  echo "Converted $file to $new_file"
}

# 사용: ./migrate-file.sh src/components/Button.js
convert_file $1

마이그레이션 체크리스트

프로젝트별 마이그레이션 완료 기준:

## TypeScript 마이그레이션 체크리스트

### 🔧 환경 설정
- [ ] tsconfig.json 설정 완료
- [ ] 빌드 도구 TypeScript 지원
- [ ] ESLint TypeScript 규칙 설정
- [ ] 에디터 TypeScript 지원 확인

### 📝 코드 변환
- [ ] 새 파일 TypeScript 작성 규칙 적용
- [ ] 유틸리티 함수 마이그레이션 완료
- [ ] API 레이어 타입 정의 완료
- [ ] 컴포넌트 Props 타이핑 완료

### 🧪 품질 보증
- [ ] 타입 에러 0개 달성
- [ ] 테스트 커버리지 유지/개선
- [ ] 빌드 시간 성능 확인
- [ ] 번들 크기 모니터링

### 👥 팀 준비도
- [ ] 팀원 TypeScript 기초 교육 완료
- [ ] 코드 리뷰 가이드라인 수립
- [ ] 문서화 완료 (README, 개발 가이드)

자주 묻는 질문 (FAQ)

Q1: TypeScript 마이그레이션에 얼마나 오랜 시간이 걸리나요?

A: 중간 규모 프로젝트(5만 라인)는 보통 3-6개월 정도 걸립니다. 팀의 경험과 전략에 따라 차이가 있어요.

Q2: 마이그레이션 중에 기능 개발을 병행할 수 있나요?

A: 점진적 마이그레이션 전략을 사용하면 기능 개발과 병행이 가능합니다. 새 기능은 TypeScript로 작성하세요.

Q3: 마이그레이션 후 성능에 영향이 있나요?

A: 런타임 성능에는 영향이 없습니다. 오히려 더 나은 최적화로 번들 크기가 줄어들 수 있어요.

Q4: 팀원 중 TypeScript 경험이 없는 경우는?

A: 마이그레이션 전에 2-3주간 기초 교육을 진행하고, 페어 프로그래밍을 활용하세요.

Q5: 마이그레이션 실패 위험을 줄이는 방법은?

A: 작은 단위로 점진적 마이그레이션, 충분한 테스트 커버리지 유지, 팀 전체의 합의와 교육이 핵심입니다.

❓ TypeScript 마이그레이션 전략 마무리

JavaScript에서 TypeScript로의 마이그레이션은 단순한 기술적 전환이 아니라 개발 문화의 변화입니다. 체계적인 계획과 점진적 접근으로 안전하게 진행한다면, 코드 품질과 개발 생산성 모두를 크게 향상시킬 수 있어요.

무엇보다 팀 전체의 공감대를 형성하고, 충분한 준비 기간을 갖는 것이 성공의 핵심입니다. 서두르지 말고 차근차근 진행하세요!

TypeScript 마스터 시리즈로 더 깊이 있는 학습을 원한다면 TypeScript 에러 디버깅 완전 가이드도 함께 확인해보세요! 💪

🔗 TypeScript 심화 학습 시리즈

TypeScript 마이그레이션을 성공적으로 완료하셨다면, 다른 고급 기능들도 함께 마스터해보세요:

📚 다음 단계 학습 가이드

📚 공식 문서 및 참고 자료