[TS] 제네릭(Generic)이란?

jiny·2025년 1월 30일

기술 면접

목록 보기
48/78

🗣️ 타입스크립트의 제네릭을 사용해본 경험이 있으신가요?

  • 의도: 제네릭에 대한 지식을 가지고 있는지 확인하는 질문

  • 팁: 쉬운 예시를 들어도 좋다.

  • 나의 답안

    네, 사용해 봤습니다.
    제네릭함수나 컴포넌트가 다양한 타입의 값을 처리할 수 있도록 하면서도, 타입 안정성을 유지하는 기능이라고 생각합니다.

    제가 진행한 프로젝트에서는 공통 페이지네이션 응답ApiResponse<T>라는 제네릭으로 한 번만 정의해 두고, 도메인에 따라 콘텐츠 타입만 교체해서 재사용했습니다.

    이렇게 하면 페이지 번호, 정렬 정보 등 응답의 공통 필드는 전부 한 타입에서 일관되게 관리하고, 실제 데이터 구조만 제네릭 매개변수로 바꾸면 되기 때문에 타입 안정성재사용성이 높아집니다.

  • 주어진 답안 (모범 답안)

    네, 제네릭을 사용하여 여러 코드에 걸쳐 일관성을 높여 재사용성유지 보수성, 안정성을 향상시킨 경험이 있습니다.
    제네릭을 사용한다면 함수 내부의 타입들이 마치 톱니바퀴가 움직이듯 척척 맞물리게 되어, 비록 작성이 어려울지언정 결과물에 대해서는 꽤나 뿌듯했습니다.

    그리고 제네릭이라는 게 사실 리액트의 useState에서 객체의 상태를 다룰 때 필요했던 거라 생각보다 쉽게 마주할 수 있어 그렇게 낯선 개념도 아니었습니다.
    개인적으로는 C나 Java에서 봤던 개념이라 사용에 익숙했을지도 모른다고 생각합니다.


📝 개념 정리

🌟 제네릭(Generic)이란?

  • 제네릭은 타입을 변수처럼 사용할 수 있는 기능이다.
  • 다양한 타입에서 재사용할 수 있는 유연한 함수, 클래스, 인터페이스, 타입을 만들 때 사용한다.
  • 제네릭을 활용하면 코드의 타입 안정성을 유지하면서도, 타입을 고정하지 않고 다양한 타입을 처리할 수 있다.

🌟 제네릭이 필요한 이유

타입스크립트는 기본적으로 정적 타입을 제공하지만, 재사용성을 높이면서도 타입 안전성을 유지하려면 제네릭이 필요하다.

  • 제네릭이 없는 경우
    다음과 같은 identity 함수가 있다고 가정하자.

    function identity(arg: any): any {
      return arg;
    }
    
    const result = identity(10);
    console.log(result.toUpperCase()); // [에러] 런타임 에러 발생
    • any 타입을 사용하면 모든 타입을 받을 수 있지만, 타입 안전성이 보장되지 않는다.
    • result가 숫자형(number)인데 toUpperCase()를 호출하면 런타임 에러가 발생한다.

🌟 제네릭의 기본 사용법

제네릭을 사용하면 함수나 클래스가 특정 타입에 종속되지 않고 유연한 타입을 가질 수 있다.

  • 제네릭 함수
    제네릭을 사용하여 identity 함수를 개선하면 다음과 같다.

    function identity<T>(arg: T): T {
      return arg;
    }
    
    // 사용 예시
    const num = identity<number>(10);
    const str = identity<string>("Hello");
    
    console.log(num.toFixed(2)); // 타입 안전
    console.log(str.toUpperCase()); // 타입 안전
    • <T>는 타입 매개변수(Type Parameter)이며, 함수가 호출될 때 T가 결정된다.
    • identity<number>(10)을 호출하면 T = number가 되고, identity<string>("Hello")를 호출하면 T = string이 된다.
    • 이제 컴파일 타임에 타입 검사가 가능하므로, 잘못된 연산을 런타임 이전에 방지할 수 있다.

🌟 제네릭의 다양한 활용법

  1. 제네릭 인터페이스
    제네릭을 사용하여 다양한 타입의 데이터를 저장하는 인터페이스를 만들 수 있다.

    interface Box<T> {
      value: T;
    }
    
    // number 타입을 지정하는 Box
    const numberBox: Box<number> = { value: 42 };
    
    // string 타입을 지정하는 Box
    const stringBox: Box<string> = { value: "Hello" };
    
    console.log(numberBox.value); // 42
    console.log(stringBox.value.toUpperCase()); // "HELLO"
    • Box<T>T 타입의 value를 가진다.
    • 이를 통해 numberBox는 number, stringBox 타입은 string 타입을 가지도록 제한할 수 있다.
  1. 제네릭 클래스
    제네릭을 활용하면 클래스도 타입에 관계없이 재사용할 수 있다.

    class DataStorage<t> {
      private data: T[] = [];
      
      addItem(item: T) {
        this.data.push(item);
      }
      
      removeItem(item: T) {
        this.data = this.data.filter(i => i !== item);
      }
      
      getItems(): T[] {
        return this.data;
      }
    }
    
    // string 타입을 저장하는 DataStorage
    const textStorage = new DataStorage<string>();
    textStorage.addItem("Hello");
    textStorage.addItem("World");
    textStorage.removeItem("Hello");
    console.log(textStorage.getItems()); // ["World"]
    
    // number 타입을 저장하는 DataStorage
    const numberStorage = new DataStorage<number>();
    numberStorage.addItem(10);
    numberStorage.addItem(20);
    console.log(numberStorage.getItems()); // [10, 20]
    • T를 사용하여 DataStorage가 여러 타입을 지원하도록 만들었다.
    • textStorage는 string 타입만 저장할 수 있으며, numberStoragenumber 타입만 저장할 수 있다.
    • 같은 클래스를 다양한 타입으로 활용할 수 있어 재사용성이 높아진다.
  1. 제네릭 타입 제한(constraints)
    제네릭을 사용할 때는 모든 타입을 허용할 수도 있지만, 특정 타입만 받도록 제한할 수도 있다.
    예를 들어, length 속성을 가진 타입만 허용하도록 제한하려면 extends 키워드를 사용한다.

    // T는 반드시 length 속성을 가져야 한다.
    function logLength<T extends { length: number }>(arg: T): void {
      console.log(arg.length);
    }
    
    // 문자열(string)은 length 속성이 있음
    logLength("Hello"); // 가능
    
    // 배열(array)도 length 속성이 있음
    logLength([1, 2, 3]); // 가능
    
    // 객체(objects)도 length 속성이 있으면 가능
    logLength({ length: 10, name: "Object" }); // 가능
    
    // 숫자(number)는 length 속성이 없음
    logLength(100); // [에러] 불가능
    • T extends { length: number }를 사용하여 length 속성이 있는 타입만 허용했다.
    • string, array, { length: number }가 있는 객체는 허용되지만, number는 허용되지 않는다.
  1. 제네릭 키 타입 (keyof 활용)
    제네릭과 keyof를 함께 사용하면 객체의 속성을 동적으로 가져올 수 있다.

    function getProperty<T, K extends keyof T>(obj: T, key: K) {
      return obj[key];
    }
    
    const person = { name: "Alice", age: 25 };
    
    console.log(getProperty(person, "name")); // "Alice"
    console.log(getProperty(person, "age")); // 25
    console.log(getProperty(person, "height")); // [오류] height는 존재하지 않음
    • keyof객체 타입의 키(key)들을 문자열 리터럴 타입으로 추출하는 연산자이다.
      즉, 객체 타입의 속성(key)들을 타입으로 가져오는 역할을 한다.
    • K extends keyof T를 사용하여 KT 객체의 속성 중 하나만 허용되도록 제한했다.
    • 따라서 존재하지 않는 속성(height)을 가져오려 하면 오류가 발생한다.

🌟 제네릭의 실전 활용 예제

  • API 응답 타입 정의
    제네릭을 활용하면 API 응답 타입을 유연하게 정의할 수 있다.

    interface ApiResponse<T> {
      status: number;
      message: string;
      data: T;
    }
    
    // 사용자 데이터 API 응답
    const userResponse: ApiResponse<{ id: number; name: string }> = {
      status: 200,
      message: "Success",
      data: { id: 1, name: "Kim" },
    };
    
    // 상품 데이터 API 응답
    const productResponse: ApiResponse<{ id: number; price: number }> = {
      status: 200,
      message: "Success",
      data: { id: 101, price: 3000 },
    };
    • ApiResponse<T>를 사용하면 다양한 데이터 타입을 처리할 수 있다.
    • userResponse는 사용자 데이터를, productResponse는 상품 데이터를 처리한다.

🌟 결론

  • 제네릭은 타입을 유연하게 유지하면서도 안정성을 보장하는 강력한 기능이다.
  • 제네릭 함수, 클래스, 인터페이스를 활용하면 타입을 고정하지 않고 다양한 타입을 처리할 수 있다.
  • 타입 제한(constraints), keyof, API 응답 타입 등 다양한 활용 방법이 존재한다.
  • 제네릭을 활용하면 재사용성이 뛰어난 타입 안전한 코드를 작성할 수 있으므로, 타입스크립트 프로젝트에서 적극적으로 활용하는 것이 좋다.

0개의 댓글