[Typescript] Generic 알아보자!

dee·2022년 12월 19일
1

typescript

목록 보기
5/5
post-thumbnail

🤔 Generic이란?

어떠한 클래스 혹은 함수에서 사용할 타입을 그 함수나 클래스를 사용할 때 결정하는 프로그래밍 기법이다.


제네릭을 사용하는 이유

  1. any 타입 지정의 문제 해결
  • 해당 타입으로 지정하면 저장하고 있는 자료의 타입이 모두 같지 않다는 문제점이 있어 런타임에서 항상 타입 검사가 필요하다.
  1. 범용성을 높여 타입 재사용
  • string같은 하나의 타입만 받으면 범용성이 떨어진다.

제네릭 사용 방법

간단하게 제네릭 사용방법에 대해 알아보자.
아래처럼 매개변수와 반환값을 같은 타입으로 반환하는 두 개의 함수가 있다. 각각 string과 number를 반환하고 있다. 두 함수를 살펴보면 똑같은 패턴을 가지고 있다. 이와 같은 함수 타입은 재사용을 높이기 위해 제네릭을 적용할 수 있다.

const getSameType1 = (str: string): string => str;
const getSameType2 = (num: number): number => num;

Type을 의미하는 대문자 T를 사용하여 함수를 호출할 때 타입을 지정할 수 있도록 할 수 있다. 이처럼 작성하게 되면 선언한 하나의 함수로 그에 맞는 타입을 체크하며 호출할 수 있다. 만약 이 부분이 모두 any로 되어 있었다면 어떤 타입의 매개변수와 반환값인지를 알 수 없었을 것이다. 타입스크립트의 장점을 잘 활용하기 위해서 제네릭을 잘 알아둘 필요가 있다.
🚨 T이외에도 다른 대문자를 사용할 수 있으며 변수 i, j, k를 사용하듯이 많이 사용하는 제네릭 문자는 T인 것이다.

const getSameType = <T>(same: T): T => same;

getSameType<string>('hello'); // hello
getSameType<number>(1); // 1

제네릭 제약 조건

위와 같이 제네릭을 사용하다보며 광범위하게 타입이 허용된다. 그래서 extends를 통해 제네릭에 제약조건을 설정할 수 있다. 이 부분에 대한 이해가 부족하여 프로젝트에 제네릭을 적용하면서 애를 먹었었다. 블로그 정리를 하면서 명확하게 이해해보자.

type Parrot = {
	name: string;
  	age: number;
}

const getBird = <T extends Parrot>(bird: T) => {
  	 // 1
	console.log(bird.name)
};

type NewBird = {
    name: string;
    age: number;
}

type Baby = {
	age: number;
}

// 2
getBird<NewBird>({name: 'nari', age: 123});

// 3 -> error
getBird<Baby>({age: 1}); 

위와 같이 타입에 제한 조건을 걸어놓으면 1번 코드 작성시 함수에 어느 정도 힌트를 주게 되고 에디터에서도 자동 완성이 가능하게 됩니다.
2번 코드와 같이 같은 키와 타입을 가진 타입을 체크할 수 있으며 3번은 다음과 같으 에러를 내뱉는다.

Type 'Baby' does not satisfy the constraint 'Parrot'. Property 'name' is missing in type 'Baby' but required in type 'Parrot'.

Baby 타입이 Parrot 타입을 만족할 수 없다는 것과 함께 어떤 프로퍼티가 빠졌는지를 알 수 있다. 이에 제네릭 제약조건은 훨씬 더 타입스크립트 같은 효율성 높은 코드를 만들 수 있다.


🥲 제네릭 적용 과정기

현재 진행중인 Next_doWork라는 프로젝트의 로그인과 회원가입을 구현하면서 여러 기능들과 UI과 겹치는 것들이 많았다. 그래서 커스텀 훅과 공통 컴포넌트로 빼내어 사용하고자 제네릭을 적용하였다.

1. AuthLayout.tsx 컴포넌트와 useAuth.ts 커스텀 훅 생성.
2. defaultUserInfo라는 데이터를 props로 받음.
🚨 defaultUserInfo는 로그인 타입이나 회원가입 타입으로 체크되어야 한다.

처음 작성한 코드는 다음과 같다. 제네릭 T, M을 사용하여 필요한 곳에 같은 타입을 지정해주려고 했는데 info.id에서 에러가 나고 있었다.

const useAuth = <T, M>(defaultUserInfo: Array<M>) => {
  const [userInfo, setUserInfo] = useState<Array<M>>(defaultUserInfo);

  const onChange = (e: ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target as HTMLInputElement;
    
    // Property 'id' does not exist on type 'M'.ts(2339)
    setUserInfo(prev => prev.map(info => (info.id === name ? { ...info, value } : info)));
  };

  return {
    //...
  };
};

export default useAuth;

위의 Array타입을 가진 userInfo state의 요소인 객체에서 id가 존재하지 않는다는 것이였다. 처음에는 훅을 사용하면서 M에 제대로된 타입을 넘겨주었는데 왜 모르는 것일까? 라고 의문을 가졌었다.

그래서 다시 타입스크립트의 관점으로 돌아가 문제를 바라보았다. 그렇게 분석한 결과 userInfo state는 확실하게 알 수 있는 타입은 배열이라는 점이였고 그랬기 때문에 prev.map()에서 타입 체크를 할 때 오류가 나지 않았다. 하지만 배열 안이 객체인 것을 모를뿐더러 id라는 프로퍼티가 있다는 것은 불분명했다. 그렇기에 타입체크에서 오류가 발생한 것이였다.

이에 위에서 공부한 제네릭 제약 조건을 설정하여 userInfo state가 객체로 이루어진 배열이고 또한 id 프로퍼티를 가지고 있다는 것을 명확하게 타입으로 지정을 해주었다. 그 결과 타입 체크에서 오류없이 잘 동작할 수 있는 것을 볼 수 있었다.

const useAuth = <T extends AuthType, M extends iDefaultUserInfo>(defaultUserInfo: Array<M>) => {
  const [userInfo, setUserInfo] = useState<Array<M>>(defaultUserInfo);

  const onChange = (e: ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target as HTMLInputElement;
    
    setUserInfo(prev => prev.map(info => (info.id === name ? { ...info, value } : info)));
  };

  return {
    //...
  };
};

export default useAuth;

참조
https://hyunseob.github.io/2017/01/14/typescript-generic/

profile
웹 프론트엔드 개발자

0개의 댓글