TypeScript / / 2024. 10. 29. 21:40

TypeScript 제네릭1

제네릭(Generic)이란?

제네릭은 타입스크립트에서 함수, 클래스, 인터페이스, 타입 등을 다양한 타입과 함께 동작하도록 만드는 기능입니다. 즉, 특정 타입에 종속되지 않고, 여러 타입에 대해 범용적으로 동작할 수 있는 코드를 작성하는 것을 목표로 합니다.

제네릭이 필요한 상황

함수에서 다양한 타입의 매개변수를 받아 그대로 반환하는 경우를 예로 들어 설명할 수 있습니다.

function func(value: any) {
  return value;
}

let num = func(10);
let str = func("string");
  • 위 코드에서는 매개변수 value의 타입을 **any**로 설정했습니다. 이로 인해 함수 func가 어떤 타입을 받든 받아들일 수 있습니다.
  • 그러나 num과 str 모두 any 타입으로 추론되므로 타입스크립트가 런타임 오류를 감지하지 못합니다. 예를 들어 num.toUpperCase()와 같은 부적절한 메서드 호출도 컴파일 오류 없이 통과되어, 결국 실행 시 오류가 발생하게 됩니다.

이를 해결하기 위해 unknown 타입을 사용할 수 있지만, 이 경우 각 변수의 실제 타입을 명시적으로 확인하고 타입 좁히기(type narrowing)를 해야 하는 번거로움이 발생합니다.

제네릭을 통한 해결

제네릭을 사용하면 이러한 문제를 쉽게 해결할 수 있습니다.

function func<T>(value: T): T {
  return value;
}

let num = func(10);        // num은 number 타입으로 추론됨
let str = func("string");  // str은 string 타입으로 추론됨
  • <T>: 여기서 T는 타입 변수로, 매개변수의 타입을 함수가 호출될 때 결정하도록 합니다.
  • 함수가 호출될 때 전달된 인수에 따라 T가 결정되고, 반환 타입도 동일하게 T로 설정됩니다.
  • func(10)을 호출하면 T는 **number**로 추론되고, 반환 타입도 number가 됩니다. 마찬가지로 func("string")을 호출하면 T는 **string**으로 추론됩니다.

이렇게 하면 함수의 매개변수로 전달된 타입을 반환 타입으로 유지할 수 있어 타입 안정성을 보장하게 됩니다.

제네릭 함수의 호출 시 타입 명시

제네릭 함수 호출 시 타입을 직접 명시하는 것도 가능합니다.

function func<T>(value: T): T {
  return value;
}

let arr = func<[number, number, number]>([1, 2, 3]);
  • 여기서 T는 [number, number, number] (튜플 타입)으로 명시적으로 설정되었습니다.
  • 이로 인해 매개변수와 반환값 모두 튜플 타입이 되며, 타입스크립트는 더 구체적인 타입을 사용하게 됩니다.

 

타입스크립트는 일반적으로 타입을 추론하며, 더 범용적인 타입으로 설정하려고 합니다. 따라서 특별히 구체적인 타입을 원할 때는 타입 변수를 직접 명시할 수 있습니다.

제네릭의 장점

  1. 타입 안정성: 제네릭은 컴파일 시 타입을 확인하므로, 런타임 오류를 줄이고 코드의 타입 안전성을 보장합니다.
  2. 재사용성: 타입에 독립적이므로 여러 타입에 대해 재사용 가능한 코드를 작성할 수 있습니다.
  3. 유연성: 제네릭을 사용하면 다양한 타입을 받아들일 수 있어 코드의 유연성이 높아집니다.

제네릭의 요약

  • 제네릭은 다양한 타입에 대해 동작하는 범용적인 함수를 만들기 위한 타입스크립트의 기능입니다.
  • any 타입을 사용하면 타입 안정성을 잃게 되고, unknown 타입을 사용하면 타입 좁히기가 번거로워지지만, 제네릭을 사용하면 호출 시 전달된 타입에 맞춰 반환 타입을 결정하여 이러한 문제를 해결합니다.
  • 제네릭을 사용할 때는 함수 뒤에 **<T>**와 같은 타입 변수를 선언하고, 이를 매개변수와 반환 타입에 활용합니다.

제네릭은 코드의 유연성, 안정성, 재사용성을 모두 높일 수 있는 강력한 기능이므로, 타입에 구애받지 않으면서도 타입 안전성을 유지하고자 할 때 활용하면 좋습니다.




제네릭(Generic) 사례 정리

**제네릭(Generic)**은 다양한 타입을 유연하게 다루기 위한 타입스크립트의 강력한 기능입니다. 이 기능을 통해 여러 타입에 대해 안전하게 작업하면서 코드 재사용성을 극대화할 수 있습니다.

아래에서는 다양한 제네릭의 사례를 통해 어떻게 제네릭을 활용할 수 있는지, 각 사례의 목적과 결과를 살펴보겠습니다.

사례 1: 두 개의 타입 변수가 필요한 경우

  • 제네릭 타입 변수를 여러 개 사용하면, 함수나 클래스에서 서로 다른 타입을 유연하게 다룰 수 있습니다.
  • 예제:
    • 이 함수는 두 개의 타입 변수 T와 U를 사용하여 두 값의 순서를 뒤바꿉니다.
    • **"1"**은 T로 string 타입, **2**는 U로 number 타입으로 추론됩니다.
    • 함수의 반환 타입은 [U, T]로 [number, string]이 됩니다.
  • function swap<T, U>(a: T, b: U): [U, T] {
      return [b, a];
    }
    
    const [a, b] = swap("1", 2);

사례 2: 다양한 배열 타입을 인수로 받는 제네릭 함수

  • 제네릭 타입을 사용하여 다양한 배열 타입을 처리할 수 있습니다.
  • 예제:
    • 함수 매개변수의 타입을 **T[]**로 설정하여 배열 타입만 받을 수 있게 제한했습니다.
    • **num**은 [0, 1, 2] 배열의 첫 번째 요소 타입인 **number**로 추론됩니다.
    • **str**은 [1, "hello", "world"] 배열의 첫 번째 요소인 **number | string**으로 추론됩니다.
  • function returnFirstValue<T>(data: T[]): T {
      return data[0];
    }
    
    let num = returnFirstValue([0, 1, 2]);           // number
    let str = returnFirstValue([1, "hello", "world"]); // number | string

사례 3: 배열의 첫 번째 요소의 타입을 반환하는 제네릭 함수

  • 배열의 첫 번째 요소의 타입만 반환하고 싶을 때는 튜플 타입과 **나머지 매개변수(...)**를 사용하여 타입을 정의할 수 있습니다.
  • 예제:
    • 여기서는 매개변수를 튜플 타입으로 지정하여 첫 번째 요소만 특정한 타입(T)을 가집니다.
    • T는 배열의 첫 번째 요소의 타입으로 결정되며, 따라서 반환 타입도 그 첫 번째 요소의 타입이 됩니다.
    • 위 코드에서는 T가 number로 추론되어 반환값의 타입도 number가 됩니다.
  • function returnFirstValue<T>(data: [T, ...unknown[]]): T {
      return data[0];
    }
    
    let str = returnFirstValue([1, "hello", "world"]); // number

사례 4: 타입 변수를 제한 (extends 사용)

  • 특정 타입에 제한을 두고 싶을 때 **extends**를 사용하여 제네릭 타입을 제한할 수 있습니다.
  • 예제:
    • T extends { length: number }: 여기서 T는 length 프로퍼티를 가진 객체 타입으로 제한됩니다.
    • 이를 통해 T는 반드시 length 프로퍼티를 가진 타입이어야 합니다.
    • 문자열(string), 배열(Array), 그리고 length 프로퍼티를 가진 객체는 모두 허용됩니다.
    • 반면, **undefined**와 **null**은 length 프로퍼티가 없으므로 오류가 발생합니다.
    • function getLength<T extends { length: number }>(data: T): number {
        return data.length;
      }
      
      getLength("123");            // ✅ 허용 (string 타입은 length 프로퍼티가 있음)
      getLength([1, 2, 3]);        // ✅ 허용 (배열은 length 프로퍼티가 있음)
      getLength({ length: 1 });    // ✅ 허용 (length 프로퍼티가 있는 객체)
      getLength(undefined);        // ❌ 오류 (length 프로퍼티가 없음)
      getLength(null);             // ❌ 오류 (length 프로퍼티가 없음)

제네릭 사용의 이점 요약

  1. 유연한 타입 지원: 제네릭을 사용하면 특정한 타입에 종속되지 않고 다양한 타입을 받아들일 수 있습니다.
  2. 타입 안전성: 제네릭은 함수나 클래스가 다룰 타입을 호출 시점에 결정하므로, 타입 안전성을 확보하면서 다양한 경우에 대응할 수 있습니다.
  3. 재사용성: 제네릭은 같은 함수나 클래스를 여러 타입에 대해 재사용 가능하게 만들어 줍니다.
  4. 제약 조건 설정: extends를 사용해 제네릭 타입에 제한을 설정할 수 있으며, 이를 통해 특정 조건을 만족하는 타입만 사용할 수 있습니다.

사례의 요약

  • 사례 1: 두 개의 타입 변수가 필요한 경우, 제네릭 타입 변수를 여러 개 사용하여 서로 다른 타입을 유연하게 다룰 수 있습니다.
  • 사례 2: 배열 타입을 인수로 받는 제네릭 함수에서, 배열의 첫 번째 요소 타입에 맞춰 반환값의 타입을 추론할 수 있습니다.
  • 사례 3: 튜플 타입과 나머지 매개변수를 이용해 첫 번째 요소의 타입만 추론하고 반환할 수 있습니다.
  • 사례 4: 타입 변수를 제한하여 특정 조건(예: length 프로퍼티가 존재)을 만족하는 타입만 허용하는 제네릭 함수를 만들 수 있습니다.

제네릭은 타입스크립트에서 코드를 재사용하고 타입 안정성을 유지하는 데 매우 유용한 기능입니다. 이를 잘 활용하면 다양한 타입에 대해 안전하고 확장 가능한 코드를 작성할 수 있습니다.

 

 

제네릭을 활용한 배열 메서드 구현 및 타입 정의

자바스크립트의 배열 메서드인 **map**과 **forEach**는 배열을 조작하는 데 매우 유용한 함수입니다. 이들을 제네릭을 이용하여 직접 구현하고 타입 정의까지 어떻게 하는지 단계별로 설명합니다.

1. Map 메서드 구현과 타입 정의

map 메서드는 배열의 각 요소에 콜백 함수를 적용한 결과로 새로운 배열을 반환하는 메서드입니다.

const arr = [1, 2, 3];
const newArr = arr.map((it) => it * 2);
// [2, 4, 6]

 

  1. 일반적인 함수 형태로 구현:
    • arr: 배열의 각 요소에 작업을 수행할 원본 배열 (unknown[] 타입).
    • callback: 각 배열 요소에 적용할 함수 ((item: unknown) => unknown).
    • 이 함수는 모든 타입의 배열에 적용할 수 있어야 하므로 unknown 타입을 사용했지만, 이후 제네릭으로 수정합니다.
    • function map(arr: unknown[], callback: (item: unknown) => unknown): unknown[] {}
  2. 제네릭 함수로 수정:
    • 타입 변수 T를 사용해 원본 배열의 타입을 추론하고, 콜백 함수의 매개변수 및 반환값 타입 T로 설정하여 타입 안전성을 확보합니다.
    • function map<T>(arr: T[], callback: (item: T) => T): T[] {
        let result = [];
        for (let i = 0; i < arr.length; i++) {
          result.push(callback(arr[i]));
        }
        return result;
      }
  3. 타입 변수 추가하기:
    • map 함수는 원본 배열과 다른 타입으로 변환도 가능합니다. 이를 위해 두 개의 타입 변수를 추가합니다.
    • function map<T, U>(arr: T[], callback: (item: T) => U): U[] {
        let result = [];
        for (let i = 0; i < arr.length; i++) {
          result.push(callback(arr[i]));
        }
        return result;
      }
     
    • T는 원본 배열 요소의 타입이고, U는 콜백 함수 반환값의 타입입니다.
    • 이렇게 하면 배열 요소의 타입과 변환된 배열의 타입이 다를 수 있습니다.
  4. 사용 예시:
    • const arr = [1, 2, 3];
      
      const stringArr = map(arr, (it) => it.toString());
      // string[] 타입의 배열 반환
      // 결과 : ["1", "2", "3"]
    • 이 코드에서는 number[] 타입의 배열을 문자열로 변환하여 string[] 타입의 배열로 만들었습니다.
    • 이렇게 제네릭을 통해 타입 변환이 유연해졌습니다.

2. ForEach 메서드 구현과 타입 정의

forEach 메서드는 배열의 모든 요소에 대해 콜백 함수를 수행하는 메서드로, 새로운 배열을 반환하지 않습니다.

const arr2 = [1, 2, 3];

arr2.forEach((it) => console.log(it));
// 출력 : 1, 2, 3

 

직접 구현하기

  1. forEach 메서드 구현:
    • function forEach<T>(arr: T[], callback: (item: T) => void): void {
        for (let i = 0; i < arr.length; i++) {
          callback(arr[i]);
        }
      }
    • 매개변수:
      • arr: 순회할 배열 (T[] 타입).
      • callback: 배열의 각 요소에 대해 수행할 함수 ((item: T) => void).
    • 반환 타입은 void이며, 새로운 배열을 반환하지 않고 단지 콜백 함수를 배열의 각 요소에 대해 호출만 합니다.
  2. 사용 예시:
    • const arr2 = [1, 2, 3];
      
      forEach(arr2, (it) => console.log(it));
      // 출력 : 1, 2, 3
    • 배열의 각 요소를 순회하면서 콜백 함수를 호출하여 출력만 수행합니다.

제네릭 메서드 타입 정의 요약

  • Map 메서드:
    • 원본 배열의 각 요소에 대해 콜백 함수를 수행하고, 그 결과를 모아 새로운 배열을 반환합니다.
    • 제네릭을 이용해 두 개의 타입 변수 T와 U를 사용하여 원본 배열의 타입 변환된 배열의 타입을 유연하게 정의합니다.
    • function map<T, U>(arr: T[], callback: (item: T) => U): U[] {
        let result = [];
        for (let i = 0; i < arr.length; i++) {
          result.push(callback(arr[i]));
        }
        return result;
      }
  • ForEach 메서드:
    • 배열의 각 요소에 대해 콜백 함수를 수행하지만, 새로운 배열을 반환하지 않습니다.
    • 제네릭을 사용하여 배열의 타입을 유지하고, 콜백 함수의 반환 타입을 **void**로 설정합니다.
    • function forEach<T>(arr: T[], callback: (item: T) => void): void {
        for (let i = 0; i < arr.length; i++) {
          callback(arr[i]);
        }
      }

제네릭 메서드를 사용하는 이유

  1. 타입 유연성: 제네릭을 사용하면 다양한 타입의 데이터를 처리할 수 있으면서도, 타입 안전성을 유지할 수 있습니다.
  2. 코드 재사용성: 제네릭을 통해 같은 함수나 메서드를 여러 타입에 대해 재사용 가능하게 만들 수 있습니다.
  3. 타입 안전성: 제네릭은 타입스크립트의 강력한 타입 추론 기능을 사용하여 런타임 오류를 줄이고 컴파일 시점에 오류를 미리 잡을 수 있습니다.

위의 설명을 바탕으로 제네릭을 사용하여 구현한 map과 forEach 메서드는 타입 안전성을 유지하면서도 유연한 타입 변환을 가능하게 합니다. 이를 통해 더욱 안정적이고 재사용 가능한 코드를 작성할 수 있습니다.

'TypeScript' 카테고리의 다른 글

TypeScript 타입조작하기  (0) 2024.11.02
TypeScript 제네릭2  (0) 2024.10.29
TypeScript 인터페이스로 구현하는 클래스  (0) 2024.10.29
TypeScript 클래스  (1) 2024.10.29
JavaScript this정리  (0) 2024.10.29
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유