타입스크립트 5.9 지금 바로 업그레이드해야 하는 이유

 

타입스크립트 5.9 지금 바로 업그레이드해야 하는 이유

 

 

타입스크립트 5.9 지금 바로 업그레이드해야 하는 이유

 

드디어 타입스크립트(TypeScript) 5.9가 공개되었는데요.

아마 이 소식을 듣고 가장 먼저 드는 생각은 '업그레이드를 해야 할까?'일 겁니다.

결론부터 말씀드리면, 대답은 '확실한 예스'입니다.

이번 5.9 버전은 단순한 점진적 업데이트가 아니거든요.

성능 개선과 개발자 중심의 새로운 기능들이 균형 있게 포함된 의미 있는 도약입니다.

이번 릴리스는 타입스크립트 팀이 커뮤니티의 목소리에 얼마나 귀 기울이고 있는지 보여주는데요.

단순히 강력한 타입 시스템을 넘어, '사용하는 즐거움'까지 고민한 흔적이 엿보이는 업데이트입니다.

특히 5.9 버전에서 주목할 만한 점은 '실질적으로 중요한' 작은 개선들에 집중했다는 점인데요.

가장 영향력 있는 변화가 항상 가장 요란한 것은 아니라는 사실을 잘 보여주는 사례입니다.

이번 글에서는 타입스크립트 5.9의 핵심 기능들을 하나씩 살펴보려고 하는데요.

새롭게 디자인된 tsc --init 명령어부터 흥미로운 import defer 구문, 확장 가능한 호버 기능, 그리고 체감 가능한 성능 개선까지, 이번 업그레이드가 왜 가치 있는지 자세히 알아보겠습니다.

 

타입스크립트 5.9 변화의 기저에 깔린 철학

타입스크립트 5.9를 보면서 가장 인상 깊었던 점은 특정 기능 하나가 아니었는데요.

오히려 이번 릴리스를 관통하는 '철학' 그 자체였습니다.

마치 타입스크립트 팀이 스스로에게 근본적으로 다른 질문을 던지는 것 같았거든요.

'어떻게 하면 개발자의 인지 부하를 줄일 수 있을까?', '개발자들이 실제로 체감할 수 있는 성능 개선은 무엇일까?', '언어 기능 간의 간극을 어떻게 메울 수 있을까?' 같은 질문들 말입니다.

이러한 생각의 전환은 우리가 더 깊이 살펴봐야 할 네 가지 핵심 영역에서 명확하게 드러납니다.

 

프로젝트 초기화 tsc --init

먼저, 그다지 흥미로워 보이지 않을 수 있는 주제부터 시작해 볼까요?

바로 이전 버전의 프로젝트 초기화 방식입니다.

개편된 tsc --init 명령어는 사소한 편의성 개선처럼 보일 수 있는데요.

하지만 실제로는 그보다 훨씬 더 중요한 의미를 담고 있습니다.

어떤 개발 도구든 '첫인상'이 중요하다는 것을 인정한 셈이거든요.

이전 버전에서 생성되던 tsconfig.json 파일은 마치 방대한 설명서와 같았습니다.

사용 가능한 모든 옵션이 주석 처리된 채 꼼꼼하게 나열되어 있었는데요.

이는 도움이 되는 동시에 사용자를 압도하는 파일이었습니다.

마치 기기를 그냥 켜고 싶을 뿐인 사람에게 200페이지짜리 설명서를 건네주는 것과 같았죠.

새로운 방식은 대신 아래와 같은 파일을 생성합니다.

{
  "compilerOptions": {
    "target": "es2020",
    "module": "node16",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

단순히 코드 라인 수가 줄어든 것만이 핵심이 아닙니다.

더 중요한 것은 현재의 '모범 사례(Best Practice)'를 반영하는 좋은 선택을 기본값으로 제공한다는 점인데요.

예를 들어 "strict": true가 기본으로 포함된 것은, 타입스크립트 팀이 엄격한 타입 검사를 예외가 아닌 '표준'으로 여겨야 한다는 메시지를 보내는 것입니다.

물론 기본 설정은 의도적으로 최소화되어 있지만, 모든 팀이 이를 바탕으로 확장해 나가야 할 기초를 마련해 줍니다.

예를 들어, 많은 팀이 배열의 안전성을 높이기 위해 다음 단계로 "noUncheckedIndexedAccess": true 옵션을 추가하곤 하는데요.

이는 아주 일반적인 다음 단계입니다.

// noUncheckedIndexedAccess 미적용 (현재 기본값)
const users = ['Alice', 'Bob'];
const thirdUser = users[2]; // 타입: string (하지만 실제로는 undefined!)
console.log(thirdUser.toUpperCase()); // 런타임 에러 발생!

// noUncheckedIndexedAccess 적용 (권장 추가 사항)
const users = ['Alice', 'Bob'];
const thirdUser = users[2]; // 타입: string | undefined
if (thirdUser) {
  console.log(thirdUser.toUpperCase()); // 안전함!
}

이러한 변화는 도구의 더 큰 그림을 보여주는데요.

'이것부터 설정하세요' 방식에서 '규칙 우선(convention-first)' 접근 방식으로의 전환입니다.

이는 크리에이트 리액트 앱(Create React App)을 성공으로 이끌고, 넥스트(Next.js)의 인기를 견인하며, 비트(Vite)와 같은 도구들이 설정보다는 빌드에 집중하고 싶은 개발자들에게 매력적으로 다가가는 것과 같은 철학입니다.

체감 가능한 성능 개선

성능 개선은 두 가지 형태로 나타나는 것 같은데요.

하나는 사용자가 실제로는 거의 알아채지 못하는 인상적인 기술적 성과이고, 다른 하나는 사용자의 일상적인 작업 흐름에 실질적인 영향을 미치는 의미 있는 개선입니다.

타입스크립트 5.9는 의심할 여지 없이 두 번째에 속하는데요.

어떻게 그런지 한번 살펴보겠습니다.

중간 타입 인스턴스화 결과를 '캐싱'한다는 것은 다소 생소한 내부 구현처럼 들릴 수 있습니다.

하지만 이 기능은 대규모 코드베이스에서 파일을 저장할 때마다 실질적인 영향을 미치거든요.

리액트 쿼리(React Query)나 티알피씨(tRPC) 같은 라이브러리에서 흔히 사용하는 복잡한 타입으로 작업할 때, 타입스크립트 컴파일러는 임시 타입 표현을 생성하고 파괴하는 데 상당한 시간을 소비합니다.

이 캐싱 최적화는 그런 오버헤드를 줄여 개발 경험을 눈에 띄게 빠르게 만들어 줍니다.

다음은 최적화가 적용되는 예시입니다.

// 인스턴스화 캐싱의 이점을 얻는 복잡한 제네릭 타입
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object
    ? DeepPartial<T[P]>
    : T[P];
};

type APIResponse<T extends Record<string, any>> = {
  data: T;
  meta: {
    timestamp: number;
    version: string;
  };
  errors?: Array<{
    field: keyof T;
    message: string;
  }>;
};

// 복잡한 중첩 타입과 함께 사용될 때, 많은 중간 인스턴스화가 발생함
type UserResponse = APIResponse<{
  user: {
    profile: {
      personal: { name: string; age: number };
      professional: { title: string; company: string };
    };
    preferences: DeepPartial<{
      notifications: { email: boolean; push: boolean };
      privacy: { public: boolean; analytics: boolean };
    }>;
  };
}>;

타입스크립트가 이와 같은 타입을 처리할 때, 수많은 중간 타입 표현을 생성합니다.

이전에는 컴파일러가 동일한 타입 조합으로 작업할 때조차 이러한 중간 타입들을 반복적으로 다시 생성했는데요.

새로운 캐싱 메커니즘은 특정 타입 인스턴스화가 이미 계산되었는지 인식하고 그 작업을 재사용합니다.

이 최적화는 타입스크립트가 실제 프로덕션 환경에서 어떻게 사용되는지에 대한 중요한 사실을 보여주는데요.

타입스크립트 애플리케이션은 단순히 기본 타입만 사용하는 것이 아닙니다.

언어의 타입 시스템을 극한까지 활용하는 제네릭 패턴, 조건부 타입, 맵드 타입에 크게 의존하고 있거든요.

5.9 버전에 포함된 다른 컴파일러 최적화(생성된 코드에서 불필요한 함수 래퍼 제거 등)와 결합되어, 이러한 개선 사항들은 타입 검사 속도를 '11% 향상'시켰습니다.

이 수치가 그리 대단해 보이지 않을 수도 있는데요.

하지만 실제로 이것이 무엇을 의미하는지 생각해보면 다릅니다.

더 반응성이 좋은 IDE 경험, 개발 중 더 빠른 피드백, 그리고 CI/CD 파이프라인 시간 단축을 의미하는 것이죠.

import defer 이해하기

import defer 구문이 도입된 것을 보면, 타입스크립트가 자바스크립트(JavaScript) 명세를 앞서 나가는 동시에 성능 문제를 해결하려는 시도를 엿볼 수 있습니다.

먼저 구문이 어떻게 생겼는지부터 보시죠.

import defer * as expensiveLibrary from "./heavy-processing-module.js";

이 간단한 구문은 자바스크립트 애플리케이션의 근본적인 문제를 해결합니다.

애플리케이션이 커질수록 모듈을 가져오고 평가하는 과정이 사용자 경험에 영향을 미칠 수 있는데요.

지연된 임포트의 동작을 보여주는 실용적인 예시는 다음과 같습니다.

// heavy-chart-module.ts
console.log("비싼 WebGL 컨텍스트 초기화 중...");

// 이 비싼 초기화는 모듈 평가 시점에 발생함
const gl = canvas.getContext('webgl2');
initializeShaders();
compileComplexMathLibrary();

export class AdvancedChartRenderer {
  render(data: ChartData) {
    // 복잡한 렌더링 로직
  }
}

// main.ts
import defer * as chartModule from "./heavy-chart-module.js";

// 애플리케이션은 즉시 시작됨 - 아직 비싼 WebGL 초기화는 일어나지 않음
console.log("앱이 준비되었습니다!");

// 나중에 사용자가 버튼을 클릭하면...
function showAdvancedCharts() {
  // 바로 지금! 비싼 초기화가 발생함
  const renderer = new chartModule.AdvancedChartRenderer();
  renderer.render(chartData);
}

하지만 이 기능의 현실을 이해하는 것이 중요한데요.

현재 import defer는 주요 브라우저나 노드(Node.js) 런타임에서 아직 지원되지 않습니다.

즉, 타입스크립트로는 코드를 작성할 수 있지만, 전문 번들러 지원이나 실험적인 런타임 플래그 없이는 프로덕션 환경에서 실패할 것이라는 의미입니다.

그렇다면 왜 이 기능이 포함된 걸까요?

특히 흥미로운 점은 타입스크립트가 이 구문으로 '무엇을 하지 않으려 하는가'인데요.

컴파일러는 import defer를 폴리필(polyfill)로 변환하지 않고, 구문을 그대로 통과시킵니다.

네임스페이스 임포트(import defer * as module)로만 제한된 것은 평가 시점을 고려하면 이해가 되는데요.

네임드 임포트는 개별 export가 언제 사용 가능해지는지에 대한 불분명한 기대를 만들 수 있기 때문입니다.

// 이는 혼란스러울 수 있음 - `heavyFunction`은 언제 평가되는가?
import defer { heavyFunction } from "./expensive-module.js";

// 이는 명확함 - 모듈 전체는 어떤 프로퍼티에 접근할 때 평가됨
import defer * as expensiveModule from "./expensive-module.js";
const result = expensiveModule.heavyFunction(); // 평가가 여기서 일어남

이는 언어 설계에 대한 확고한 관점을 보여주는데요.

때로는 트랜스파일러(transpiler)가 할 수 있는 최선의 일은 그저 길을 비켜주고 기본 플랫폼이 성장하도록 두는 것일 수 있습니다.

확장 가능한 호버 기능

확장 가능한 호버 기능은 어쩌면 가장 덜 중요해 보이는 추가 기능일 수 있는데요.

하지만 이 기능은 개발자 경험이란 '발견 가능성'에 관한 것이기도 하다는 인식을 보여줍니다.

복잡한 타입은 안전성과 표현력을 제공하지만, 탐색하기 복잡할 수도 있거든요.

마우스를 올렸을 때 타입 정보를 직접 확장하고 축소할 수 있게 되면서, 탐색 과정이 훨씬 매끄러워졌습니다.

리액트 훅 폼(React Hook Form)과 같은 라이브러리로 작업할 때 흔히 겪는 시나리오를 한번 보시죠.

import { useForm } from 'react-hook-form';

// 이전에는 `control`에 마우스를 올리면 다음과 같이 표시되었음:
// control: Control<FieldValues, any>
// 이는 거의 아무런 유용한 정보도 주지 못했죠

const { control, handleSubmit } = useForm<{
  username: string;
  email: string;
  preferences: {
    newsletter: boolean;
    notifications: Array<{
      type: 'email' | 'push';
      enabled: boolean;
    }>;
  };
}>();

확장 가능한 호버 기능 덕분에, 이제 개발자들은 컨텍스트를 잃거나 여러 파일을 열지 않고도 편집기에서 직접 이런 복잡한 타입을 깊이 파고들어 전체 구조를 볼 수 있습니다.

저는 타입스크립트에 대한 자신감이 복잡한 타입을 이해하고 탐색하는 능력과 정비례한다는 것을 느꼈는데요.

팀원들이 타입 정의를 빠르게 탐색할 수 있을 때, 고급 타입스크립트 기능을 더 적극적으로 사용하게 됩니다.

호버 기능은 이를 아름답게 보완합니다.

다음은 실용적인 예시입니다.

'use client';
import { useForm } from 'react-hook-form';

// 확장 가능한 호버 기능을 잘 보여줄 복잡한 중첩 폼 타입
interface UserRegistrationForm {
  username: string;
  email: string;
  password: string;
  profile: {
    firstName: string;
    lastName: string;
    dateOfBirth: string;
    address: {
      street: string;
      city: string;
      state: string;
      zipCode: string;
      country: string;
    };
  };
  preferences: {
    newsletter: boolean;
    notifications: Array<{
      type: 'email' | 'push' | 'sms';
      enabled: boolean;
      frequency: 'immediate' | 'daily' | 'weekly' | 'never';
      categories: Array<{
        name: string;
        subscribed: boolean;
        priority: 'low' | 'medium' | 'high';
      }>;
    }>;
    privacy: {
      profileVisible: boolean;
      dataSharing: {
        analytics: boolean;
        marketing: boolean;
        thirdParty: boolean;
        retention: {
          duration: number;
          unit: 'days' | 'months' | 'years';
          autoDelete: boolean;
        };
      };
    };
  };
  socialMedia: Record<string, {
    platform: 'twitter' | 'facebook' | 'linkedin' | 'github';
    username: string;
    isPublic: boolean;
    verificationStatus: 'verified' | 'pending' | 'unverified';
    metadata: Record<string, any>;
  }>;
}

export function ReactHookFormDemo() {
  // 여기서 'control'에 마우스를 올리면 확장 가능한 Control<UserRegistrationForm, any> 타입이 표시됨
  // 이를 확장하여 전체 UserRegistrationForm 구조를 볼 수 있음
  const { control, handleSubmit, formState: { errors } } = useForm<UserRegistrationForm>({
    defaultValues: {
      username: '',
      email: '',
      password: '',
      profile: {
        firstName: '',
        lastName: '',
        dateOfBirth: '',
        address: {
          street: '',
          city: '',
          state: '',
          zipCode: '',
          country: 'US'
        }
      },
      preferences: {
        newsletter: false,
        notifications: [{
          type: 'email',
          enabled: true,
          frequency: 'daily',
          categories: [{
            name: 'updates',
            subscribed: true,
            priority: 'medium'
          }]
        }],
        privacy: {
          profileVisible: true,
          dataSharing: {
            analytics: false,
            marketing: false,
            thirdParty: false,
            retention: {
              duration: 365,
              unit: 'days',
              autoDelete: true
            }
          }
        }
      },
      socialMedia: {}
    }
  });
}

이해를 돕기 위한 GIF 이미지입니다.

 

이 기능은 타입스크립트 개발이 종종 내가 작성하지 않은 타입, 내가 유지보수하지 않는 라이브러리, 그리고 처음 마주하는 문제를 해결하는 과정의 연속이라는 점을 인정하는 것과 같습니다.

컨텍스트를 잃지 않고, 여러 파일을 넘나들거나 여러 편집기를 열지 않고도 복잡한 타입을 이해할 수 있는 능력은 정말 생산성을 높여주는 추가 기능입니다.

앞으로의 전망 타입스크립트의 다음 단계는

이 글은 단순히 타입스크립트 5.9로 업그레이드해야 하는지에 대한 답을 제시하는 데에만 초점을 맞추지 않았는데요.

이번 개선 사항들을 고려하면 대부분의 팀에게는 업그레이드가 당연한 결정이기 때문입니다.

더 흥미로운 질문은 바로 이것입니다.

'이번 릴리스는 자바스크립트 도구와 개발자 경험의 방향에 대해 우리에게 무엇을 말해주고 있는가?'

타입스크립트 5.9는 우리가 기존 작업 흐름을 개선하는 것이 획기적인 새 기능만큼이나 중요해지는 단계에 접어들고 있음을 시사하는데요.

가장 영향력 있는 변화가 항상 가장 눈에 띄는 것은 아니라는 점을 인정한 것입니다.

때로는 도구가 할 수 있는 최선의 일은 사용자가 정말로 중요한 문제 해결에만 집중할 수 있도록 도와주는 것일 겁니다.