ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 인덱스드 시그니처 (Index Signatures)와 인덱스드 접근 타입 (Indexed Access Types)
    인사이트 2025. 3. 27. 13:47

    JavaScript의 유연성과 TypeScript의 정적 타입 시스템 사이의 간극을 메우기 위한 것들을 이번 톺아보기 스터디에서 주로 다루고있는데,

    오늘은 인덱스드 시그니처 (Index Signatures)와 인덱스드 접근 타입 (Indexed Access Types)에 대해 이야기해보려한다.

     

    인덱스드 시그니처 (Index Signatures)

     

    JavaScript의 객체는 속성 이름을 동적으로 추가하거나 변경할 수 있는 매우 유용한 자료 구조이다. 하지만 타입스크립트와 같은 정적 타입 언어에서는 객체의 속성 이름과 타입을 미리 정의하는 것이 일반적이다.

     

    하지만, API 응답 데이터 등 속성 이름이 런타임에 결정되거나 예측하기 어려운 경우가 많다보니, 타입스크립트에서 모든 가능한 속성 이름을 정의하는 것은 매우 번거롭다.

     

    인덱스드 시그니처는 JavaScript의 유연성을 타입스크립트 내에서도 수용할 수 있도록 해준다. 특정 타입의 키와 값의 형태를 정의해두면, 해당 형태를 따르는 어떤 이름의 속성이라도 허용할 수 있게 된다. 이를 통해 동적인 속성 접근에 대한 타입 안정성을 확보할 수 있다. (개인적으로 any 대신에 주로 쓰는 방법이긴하다.)

     

    이벤트 배너에 어떤 정보들이 들어올건지 예측할 수 없는 상황에서, 들어올 수 있는 가짓수를 EventBannerData의 type으로 정의하고, 다양한 타입에 배너 데이터가 들어왔을 때 경우를 컴포넌트 작성 전 테스트해보는 함수를 간략하게 짜본 예시다.

     

    인덱스드 시그니처를 이용해 테스트한 이유는 아래와 같다.

     

    • 다양한 배너 형태 처리: 이미지, 텍스트, 비디오 등 다양한 형태의 배너는 각각 다른 속성을 필요로 하기에 인덱스드 시그니처를 사용하면 각 배너 데이터에 필요한 속성을 유연하게 추가하려고했다.
    • 확장성: 새로운 형태의 배너가 추가되더라도 기존 코드를 크게 추가하고 싶지 않았다.
    • 타입 안전성: 배너 데이터의 값은 정의된 타입 내에서 관리되므로 유연하지만 데이터의 일관성을 유지하되, type 속성을 통해 각 배너 타입에 맞는 속성이 존재하는지 추가적인 검사를 할 수 있게하였다.

     

    interface EventBannerData {
      [key: string]: string | number | boolean | object | Array<any>; // 배너 데이터의 키는 문자열, 값은 다양한 타입 허용
      type: 'image' | 'text' | 'video';
    }
    
    function displayBanner(bannerData: EventBannerData) {
      console.log(`배너 타입: ${bannerData.type}`);
      for (const key in bannerData) {
        if (bannerData.hasOwnProperty(key) && key !== 'type') {
          console.log(`${key}: ${bannerData[key]}`);
        }
      }
    
      if (bannerData.type === 'image' && bannerData.imageUrl) {
        console.log(`이미지 URL: ${bannerData.imageUrl}`);
      } else if (bannerData.type === 'text' && bannerData.message && bannerData.backgroundColor) {
        console.log(`메시지: ${bannerData.message}`);
        console.log(`배경색: ${bannerData.backgroundColor}`);
      } else if (bannerData.type === 'video' && bannerData.videoUrl && bannerData.autoPlay !== undefined) {
        console.log(`비디오 URL: ${bannerData.videoUrl}`);
        console.log(`자동 재생: ${bannerData.autoPlay}`);
      }
    }
    
    // 이미지 배너
    const imageBanner: EventBannerData = {
      type: 'image',
      imageUrl: '/images/event1.jpg',
      altText: '여름맞이 특별 할인',
      linkUrl: '/events/summer-sale',
    };
    
    // 텍스트 배너
    const textBanner: EventBannerData = {
      type: 'text',
      message: '오늘 하루만! 전 상품 20% 할인!',
      backgroundColor: '#f0f0f0',
      textColor: '#333',
      showCloseButton: true,
    };
    
    // 비디오 배너
    const videoBanner: EventBannerData = {
      type: 'video',
      videoUrl: '/videos/promo.mp4',
      autoPlay: true,
      loop: false,
      muted: true,
    };

     

    인덱스드 접근 타입 (Indexed Access Types)

     

    타입스크립트에서 복잡한 객체 구조를 다루다 보면, 특정 속성의 타입을 다른 곳에서 재사용하거나 참조해야 하는 경우가 많다. 특정 속성의 타입을 직접 명시하거나, 인터페이스나 타입을 다시 정의하게 된다면, 이는 코드 중복을 야기하고, 원본 타입이 변경될 때마다 수정해야 하는 번거로움이 있다.

     

    인덱스드 접근 타입은 기존에 정의된 타입의 특정 속성의 타입을 마치 속성에 접근하는 것처럼 간단한 문법으로 가져와서 사용할 수 있도록 해주고 이를 통해 코드의 재사용성을 높이고, 타입 정의를 한 곳에서 관리하여 유지보수를 용이하게 합니다. 예를 들어, 함수의 파라미터 타입이 특정 객체의 속성 타입과 같아야 할 때 유용하게 사용될 수 있다.

     

    인덱스드 접근 타입은 keyof와 함께 사용하고, 특정 속성 타입을 추출하거나, 모든 키의 타입을 얻을 때 사용한다.

    type Product = {
      id: number;
      name: string;
      price: number;
      category?: string;
    };
    
    function getPropertyValue<T, K extends keyof T>(obj: T, key: K): T[K] {
      return obj[key];
    }
    
    const sampleProduct: Product = { id: 1, name: '노트북', price: 1000000 };
    
    function processProduct(product: Product, keyName: keyof Product) {
      const value = getPropertyValue(product, keyName);
      console.log(`${keyName}: ${value}`);
    }
    
    processProduct(sampleProduct, 'name');
    processProduct(sampleProduct, 'price');
    
    // 만약 아래처럼 직접 접근하고 keyName이 잘못된 값이라면 런타임 에러 발생 가능성
    function processProductUnsafe(product: Product, keyName: string) {
      // TypeScript는 keyName이 'Product'의 키인지 컴파일 시점에 알 수 없음
      const value = product[keyName as keyof Product];
      console.log(`${keyName}: ${value}`);
    }
    
    processProductUnsafe(sampleProduct, 'nam'); // 컴파일 에러는 안 나지만 런타임에 undefined가 나올 수 있음
    • getPropertyValue: <T, K extends keyof >은  getPropertyValue 함수가 어떤 타입의 객체 obj와 그 객체의 키 중 하나인 key를 인자로 받도록 강제한다. 즉, 존재하지 않는 키를 넘겨서 런타임 에러가 발생하는 것을 막아준다.
    • processProduct: keyName이 keyof Product 타입으로 제한되어 있기 때문에, processProduct 함수를 호출할 때 Product 타입에 존재하는 키만 넘길 수 있다.  getPropertyValue 함수 내부에서도 이 제한을 활용하여 안전하게 속성 값에 접근 할 수 있다.
    • processProductUnsafe: keyName을 단순한 string 타입으로 받으면 TypeScript는 keyName이 Product의 실제 키인지 컴파일 시점에 알 수 없다. 따라서 product[keyName]으로 접근하려면 as keyof Product가 필요하며, 만약 keyName에 잘못된 값이 들어오면 런타임에 undefined가 반환될 수 있다.

    결론

    getPropertyValue같은 강제하는 함수와 keyof를 함께 사용하는 패턴은 키 값이 동적으로 결정될 때 (변수로 전달될 때) 컴파일 시점에 타입 안전성을 확보하는 데 더 큰 이점을 제공한다.  직접 문자열로 키를 명시하는 경우에는 TypeScript 자체적으로 오류를 잡아주지만, 변수를 사용할 때는 keyof를 통한 타입 제한이 중요해진다.

Designed by Tistory.