migration2025-02-18·9 min·192/348

TypeScript Strict Mode 마이그레이션: 점진적 타입 안전성 확보

TypeScript Strict Mode로의 마이그레이션 전략과 점진적 적용 방법을 설명합니다.

TypeScript Strict Mode 마이그레이션

Introduction

TypeScript Strict Mode는 컴파일러의 타입 검사를 강화하여 런타임 에러를 사전에 방지할 수 있게 해주는 기능입니다. 기존 JavaScript 코드베이스를 TypeScript로 마이그레이션할 때 Strict Mode를 한 번에 활성화하는 것은 매우 어려울 수 있습니다. 이 글에서는 점진적인 마이그레이션 전략과 실제 적용 방법을 다루겠습니다.

Environment

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": false,
    "noImplicitAny": false,
    "strictNullChecks": false,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}
# TypeScript 버전 확인
npx tsc --version
# 5.3.3

Problem

Strict Mode 비활성화 상태의 문제점:

// noImplicitAny: false 상태
function processUser(user) {
  return user.name.toUpperCase();  // user의 타입이 any
  // 컴파일 에러 없음
  // 런타임: user가 null이면 TypeError 발생!
}

// strictNullChecks: false 상태
function getUser(id: number) {
  const users = [{ id: 1, name: 'Joel' }];
  return users.find(u => u.id === id);
  // 반환 타입: { id: number; name: string } | undefined
  // strictNullChecks가 꺼지면 undefined 체크 불필요로 간주
}

const user = getUser(999);
console.log(user.name);  // 컴파일 에러 없음, 런타임: Cannot read property 'name' of undefined
# 에러 로그
$ npx tsc --noEmit
# TypeScript 컴파일 에러 없음 (strict: false)
# 런타임에 에러 발생 가능

Analysis

Strict Mode 관련 에러를 분석했습니다:

# Strict Mode 활성화 시 에러 수 확인
npx tsc --strict --noEmit 2>&1 | wc -l
# 2847개 에러!

# 에러 유형 분석
npx tsc --strict --noEmit 2>&1 | grep "TS" | sort | uniq -c | sort -rn
# TS7006: Parameter 'x' implicitly has an 'any' type  (1234)
# TS2531: Object is possibly 'null'  (567)
# TS2345: Argument of type 'X' is not assignable  (345)
# TS18047: 'x' is possibly 'null'  (234)
// 각 strict 옵션별 에러 분석
// noImplicitAny: 1234개
// strictNullChecks: 801개
// strictFunctionTypes: 234개
// strictBindCallApply: 123개
// noImplicitThis: 56개
// alwaysStrict: 0개

Solution

1단계: 점진적 옵션 활성화

// tsconfig.json - 1단계
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": false,
    
    // 점진적 활성화 옵션
    "noImplicitAny": true,
    "strictNullChecks": false,
    "strictFunctionTypes": false,
    "strictBindCallApply": false,
    "noImplicitThis": false,
    "alwaysStrict": false,
    "useUnknownInCatchVariables": false,
    "exactOptionalPropertyTypes": false,
    "noPropertyAccessFromIndexSignature": false
  }
}

2단계: noImplicitAny 해결

// before: 암시적 any 타입
function processUser(user) {
  return user.name.toUpperCase();
}

// after: 명시적 타입 정의
interface User {
  id: number;
  name: string;
  email?: string;
}

function processUser(user: User): string {
  return user.name.toUpperCase();
}

// unknown 타입 활용
function parseJSON(input: unknown): User {
  if (typeof input !== 'object' || input === null) {
    throw new Error('Invalid input');
  }
  
  const obj = input as Record;
  
  if (typeof obj.id !== 'number' || typeof obj.name !== 'string') {
    throw new Error('Invalid user object');
  }
  
  return {
    id: obj.id,
    name: obj.name,
    email: typeof obj.email === 'string' ? obj.email : undefined
  };
}

3단계: strictNullChecks 해결

// before: null 체크 누락
function getUser(id: number) {
  const users: User[] = [{ id: 1, name: 'Joel' }];
  return users.find(u => u.id === id);
}

const user = getUser(999);
console.log(user.name);  // TS2531: Object is possibly 'null'

// after: 안전한 null 처리
function getUser(id: number): User | undefined {
  const users: User[] = [{ id: 1, name: 'Joel' }];
  return users.find(u => u.id === id);
}

const user = getUser(999);
if (user) {
  console.log(user.name);  // OK
} else {
  console.log('User not found');
}

// Optional chaining 활용
const userName = user?.name ?? 'Unknown';

// Non-null assertion (신중하게 사용)
const guaranteedUser = getUser(1)!;  // null 아님을 단언

4단계: 전체 Strict Mode 활성화

// tsconfig.json - 최종 상태
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}
// 전체 Strict Mode 적용 후
interface ApiResponse {
  data: T;
  status: number;
  message?: string;
}

async function fetchData(url: string): Promise> {
  const response = await fetch(url);
  
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }
  
  const result: ApiResponse = await response.json();
  return result;
}

// 타입 안전성 보장
const result = await fetchData('/api/user/1');
if (result.status === 200) {
  console.log(result.data.name);
} else {
  console.error(result.message ?? 'Unknown error');
}

Lessons Learned

  1. 점진적 접근: 한 번에 Strict Mode를 활성화하지 말고, 옵션별로 점진적으로 적용해야 합니다
  2. noImplicitAny 우선: 가장 많은 에러를 발생시키는 noImplicitAny부터 해결하는 것이 효과적입니다
  3. 类型守卫 활용: unknown 타입과 타입 가드를 활용하여 안전한 타입 단언을 수행해야 합니다
  4. CI/CD 통합: TypeScript 컴파일 에러를 CI/CD 파이프라인에서 검사하여 새 코드에서 회귀를 방지해야 합니다
  5. 팀 교육: Strict Mode 마이그레이션은 팀원들의 TypeScript 이해도 향상과 함께 진행하는 것이 효과적입니다

이 블로그는 외부 스폰서십, 제휴 마케팅 또는 광고 수익을 받지 않습니다.