제네릭

박찬미·2024년 3월 19일
0

제네릭이란

  • 정적 언어에서 다양한 타입 간에 재사용을 높이기 위해 사용하는 문법
  • 타입스크립트도 정적 타입을 가지는 언어이기 때문에 제네릭 문법 지원
  • 함수, 타입, 클래스 등에서 내부적으로 사용할 타입을 미리 정해두지 않고 타입 변수를 사용해서 해당 위치를 비워, 실제로 그 값을 사용할 때 외부에서 타입 변수 자리에 타입을 지정하여 사용하는 방식
타입스크립트 변수는 일반적으로 <T>와 같이 꺾쇠괄호 내부에 정의되며, 
사용할 때는 함수에 매개변수를 넣는 것과 유사하게 원하는 타입을 넣어주면 된다.

type ExampleArrayType<T>= T[];

const array1: ExampleArrayType<string>=["치킨", "피자", "우동"]

타입을 명시하는 부분을 생략하면 컴파일러가 인수를 보고 타입을 추론해준다.
예제야

제네릭 사용법

1. 함수의 제네릭

어떤 함수의 매개변수나 반환 값에 다양한 타입을 넣고 싶을 때 제네릭을 사용할 수 있다.

TypeORM 라이브러리란?
Node.js위에서 작동하며 TypeScript를 사용할 수 있는 ORM라이브러리이다.

ORM라이브러리란?
ORM은 Object Relational Mapping 즉, 객체-관계 매핑의 줄임말이다.
ORM(객체 관계 매핑)은 데이터베이스와 객체 지향 프로그래밍 언어 간의 데이터를 변환하는 프로그래밍 기술 또는 소프트웨어 디자인 패턴...

Repository: TypeORM에서 제공하는 제네릭 타입으로, 주어진 형식 T에 대한 데이터 액세스를 위한 일반적인 저장소


function ReadOnlyRepository<T>(target: ObjectType<T> | EntitySchema<T> | string): Repository<T> {
  	return getConnection("ro").getRepository(target);
}

2. 호출시그니처의 제네릭

호출 시그니처(Call Signature)는 타입스크립트의 함수 타입 문법으로,
함수의 매개변수와 반환 타입을 미리 선언하는 것을 말한다.

호출 시그니처를 사용할 때 제너릭 타입을 어디에 위치시키는지에 따라 타입의 범위와 제너릭 타입을 언제 구체 타입으로 한정할지를 결정할 수 있다.

배민 선물하기 팀의 호출 시그니처 제네릭 활용 예시
템플릿이라고 생각하면 편한 것인가...
Recoil은 React 애플리케이션에서 상태를 관리하기 위한 라이브러리다.

interface useSelectPaginationProps<T> {
  	categoryAtom: RecoilState<number>;
  	filterAtom: RecoilState<string[]>;
    sortAtom:RecoilState<SortType>;
  	fetcherFunc: (props: CommonListRequest) => Promise<DefaultResponse<ContentListResponse<T>>>;
}

배민커머스웹프론트개발팀 활용 예시

export type UseReaquesterHookType = <RequestData = void, ResponseData = void>(baseURL?:
  string | Headers,
  defaultHeader? :  Headers)=> [RequestStatus, Requester<RequestData, ResponseData>];

3. 제네릭 클래스

제네릭 클래스는 외부에서 입력된 타입을 클래스 내부에 적용할 수 있는 클래스이다.

class LocalDB<T> {
  	// ...
  	async put(table: string, row: T): Promise<T> {
      	return new Promise<T>((resolved, rejected) => {
          	/* T 타입의 데이터를 DB에 저장 */
        });
    }
  
  	async get(table: string, key: any): Promise<T> {
      	return new Promise<T>((resolved, rejected) => {
          	/* T 타입의 데이터를 DB에서 가져옴 */
        });
    }
  	
  	async getTable(table: string): Promise<T[]> {
      	return new Promise<T[]>((resolved, rejected) => {
          	/* T 배열 (T[]) 타입의 데이터를 DB에서 가져옴 */
        });
    }
}

export default class IndexedDB implements ICachesStore {
  	private _DB?: LocalDB<{ key: string; value: Promise<Record<string, unknown>>; cacheTTL: number }>;
  
 	private DB() {
      	if (!this._DB) {
          	this._DB = new LocalDB("localCache", { ver: 6, tables: [{ name: TABLE_NAME, keyPath: "key" }] });
       

클래스 이름 뒤에 타입 매개변수인 T를 선언.
T는 메서드의 매개변수나 변환 타입으로 사용될 수 있음.
LocalDB 클래스는 외부에서 .......?
{key: string; value: Promise<Record<string,unknown>>; cacheTTL: number } 타입을 받아들여서 클래스 내부에서 사용될 제네릭 타입으로 결정된다.

제네릭 클래스를 사용하면 클래스 전체에 걸쳐 타입 매개변수가 적용된다.
특정 메서드만 대상으로 제너릭을 적용하려면 해당 메서드를 제네릭 메서드로 선언하면 된다.

4. 제한된 제네릭

타입스크립트에서 제한된 제네릭은 타입 매개변수에 대한 제약 조건을 설정하는 기능을 말한다.

예를 들어 string 타입으로 제약하려면 타입 매개변수는 특정 타입을 상속(extends)해야한다.

type ErrorRecord<Key extends string> = Exclude<Key, ErrorCodeType> extends never 
  ? Partial<Record<Key, boolean>> 
  : never;

5. 확장된 제네릭

제네릭 타입은 여러 타입을 상속받을 수 있으며 타입 매개변수를 여러 개 둘 수도 있다.

 <Key extends string> //타입을 이런 식으로 제약해 버리면 유연성을 잃어버린다.

유연성을 잃지 않으면서 제약해야할 때는 타입 매개변수에 유니온 타입을 상속해서 선언하면된다.

 <K extends string | number>

유니온 타입으로 T가 여러 타입을 받게 할 수는 있지만 타입 매개변수가 여러 개일 때는 처리할 수 없다. -> 매개변수 하나 더 추가해 선언한다.

6. 제네릭 예시

제네릭의 장점은 다양한 타입을 받게 함으로써 코드를 효율적으로 재사용할 수 있는 것이다.
현업에서 제일 많이 활용할 때는 API 응답 값의 타입을 지정할 때이다.
api 응답값에 MobileApiResponse을 활용해서 재사용할 수 있다.

   
export interface MobileApiResponse<Data> {
  	data: Data;
	statusCode: string;
  	statusMessage?: string;
}

export const fetchPriceInfo = (): Promise<MobileApiResponse<PriceInfo>> => {
  const priceUrl = "https: ~~~"; // url 주소

  return request({
    method: "GET",
    url: priceUrl,
  });
};

export const fetchOrderInfo = (): Promise<MobileApiResponse<Order>> => {
  const orderUrl = "https: ~~~"; // url 주소

  return request({
    method: "GET",
    url: orderUrl,
  });
};

제네릭을 굳이 사용하지 않아도 되는 타입

제네릭이 필요하지 않을 때 사용하면 코드 길이만 늘어나고 가독성을 해칠 수 있다.

  
type GType<T> = T;
type RequirementType = "USE" | "UN_USE" | "NON_SELECT";
interface Order {
	getRequirement(): GType<RequirementType>;
}

GType이 다른 곳에는 사용되지 않고 getRequirement 함수의 반환 값 타입으로만 사용되고 있으면 GType이라는 이름도 맞지 않게 되고, 굳이 제네릭 사용하는 것이 아니라 매개변수를 그냥 선언한 것과 같은 기능을 하게 되므로 아래와 같이 바로 적용해주면 훨씬 간단하다.

  
type RequirementType = "USE" | "UN_USE" | "NON_SELECT";
interface Order {
	getRequirement(): RequirementType;
}

any 사용하기

제네릭은 코드의 재사용성을 높이고 타입 추론을 하는데 사용된다.
any를 사용하면 모든 타입을 허용하기 때문에 사실상 제네익을 포함해 타입을 지정하는 의미가 사라지게된다.

만약에 내가 작성한 코드를 다른 개발자가 쉽게 이해하지 못하고 있다면...제네릭 오남용하고 있는 것은 아닌지 검토해봐야한다...

profile
우당탕탕

0개의 댓글