제네릭이란

cdwde·2022년 10월 3일
0
post-thumbnail

✅ 제네릭(Generics)의 사전적 정의

제네릭은 데이터의 타입을 일반화하는 것을 뜻한다. 자료형을 정하지 않고 여러 타입을 사용할 수 있게 해준다.
즉, 선언 시점이 아니라 생성 시점에 타입을 명시하여 하나의 타입만이 아닌 다양한 타입을 사용할 수 있도록 하는 기법으로, 재사용성이 높은 컴포넌트를 만들 때 자주 활용되는 특징이다.
한 가지 타입보다 여러가지 타입에서 동작하는 컴포넌트를 생성하는데 사용된다.

✅ 제네릭의 예시

function getText(text) {
  return text;
}

위 함수는 text라는 파라미터에 값을 넘겨 받아 text를 반환해준다.

여기서 제네릭 기본 문법을 적용해보자.

function getText<T>(text: T): T {
  return text;
}

getText<string>('jjae');
getText<number>(10);
getText<boolean>(true);

T는 제네릭을 선언할 때 관용적으로 사용되는 식별자로 타입 파라미터라고 한다.

getText<string>('jjae') 호출 시 함수에서 제네릭이 어떻게 동작하는지 살펴보자.
먼저 위 함수에서 제네릭 타입이 <string>이 되는 이유는 getText() 함수를 호출할 때 제네릭 값으로 string을 넘겼기 때문이다.
그리고 함수의 인자로 jjae를 넘기면 getText 함수는 아래와 같이 타입을 정의한 것과 같다.

function getText<string>(text: string): string {
  return text;
}

✅ 제네릭을 사용하는 이유

function logText(text: string): string {
  return text;
}

위 코드는 인자를 하나 넘겨 받아 반환해주는 함수이다. 여기서 함수의 인자와 반환 값은 모두 string으로 지정되어 있지만 여러 가지 타입을 허용하고 싶다면 any를 사용할 수 있다.

function logText(text: any): any {
  return text;
}

이렇게 타입을 바꿀 경우, 함수의 인자로 어떤 타입이 들어갔고 어떤 값이 반환되는지를 모른다. any라는 타입은 타입 검사를 하지 않기 때문이다.

이러한 문제를 해결할 수 있는게 제네릭이다.

function logText<T>(text: T): T {
  return text;
}

// 1
const text = logText<string>("Hello");
// 2
const text = logText("Hello");

보통 두 번째 방법이 코드도 더 짧고 가독성이 좋아 흔히 사용되지만, 복잡한 코드에서 타입 추정이 어렵다면 첫 번째 방법을 사용하면 된다.

✅ 제네릭 타입 변수

제네릭을 사용하기 시작하면 컴파일러에서 인자에 타입을 넣어달라는 경고를 보게 된다.

function logText<T>(text: T): T {
  console.log(text.length);	//Error: T doesn't have .length
  return text;
}

위 코드를 변환하려고 하면 컴파일러에서 에러를 발생시킨다. text.length가 있다는 단서가 없기 때문이다.

위 제네릭 코드의 의미를 살펴보면 함수의 인자와 반환 값에 대한 타입을 정하진 않았지만, 입력 값으로 어떤 타입이 들어왔고 반환 값으로 어떤 타입이 나가는지 알 수 있다. 따라서, 함수의 인자와 반환 값 타입에 마치 any를 지정한 것과 같은 동작을 한다는 것을 알 수 있다. 그래서 인자에 number 타입을 넘기더라도 에러가 나지는 않는다. 이러한 특성 때문에 현재 인자인 text에 문자열이나 배열이 들어와도 아직은 컴파일러 입장에서는 .length를 허용할 수가 없다.

그래서 이런 경우에는 제네릭에 타입을 줄 수 있다.

function logText<T>(text: T[]): T[] {
  console.log(text.length);
  return text;
}

위 코드는 일단 T라는 변수 타입을 받고, 인자 값으로는 배열 형태의 T를 받는다.

function logText<T>(text: Array<T>): Array<T> {
  console.log(text.length);
  return text;
}

✅ 제네릭 타입

function logText<T>(text: T): T {
  return text;
}

//1
let str: <T>(text: T) => T = logText;
//2
let str: {<T>(text: T): T} = logText;

위와 같은 변형 방식으로 제네릭 인터페이스 코드도 작성할 수 있다.

interface GenericLogTextFn {
  <T>(text: T): T;
}

function logText<T>(text: T): T {
  return text;
}

let myString: GenericLogTextFn = logText;

만약 인터페이스 인자 타입을 강조하고 싶으면 아래처럼 변경할 수 있다.

interface GenericTextFn<T> {
  (text: T): T;
}

function logText<T>(text: T): T {
  return text;
}

let myString: GenericTextFn<string> = logText;

✅ 제네릭 클래스

class GenericMath<T> {
  pi: T;
  sum: (x: T, y: T) => T;
}

let math = new GenericMath<number>();

제네릭 클래스를 선언할 때 클래스 이름 오른쪽에 <T>를 붙여준다. 그리고 해당 클래스로 인스턴스를 생성할 때마다 타입에 어떤 값이 들어갈 지 지정하면 된다.

✅ 제네릭 제약 조건

interface LengthWise {
  length: number;
}

function logText<T extends LengthWise>(text: T): T {
  console.log(text.length);
  return text;
}

위와 같이 작성하게 되면 타입에 대한 강제는 아니지만 length에 동작하는 인자만 넘겨받을 수 있게 된다.

logText(10);	//Error, 숫자 타입에는 length가 존재하지 않기에 오류 발생
logText({length: 0, value: 'hi'})	//text.length 코드는 객체의 속성 접근과 같이 동작하므로 오류 x

객체의 속성을 제약하는 방법

두 객체를 비교할 때도 제네릭 제약 조건을 사용할 수 있다.

function getProperty<T, O extends keyof T>(obj: T, key: O) {
  return obj[key];
}

let obj = {a: 1, b: 2, c: 3};

getProperty(obj, "a");
getProperty(obj, "z");	//Error, z는 a,b,c 속성에 해당하지 않음

제네릭 선언 시 <O extends keyof T> 부분에서 첫 번째 인자로 받는 객체에 없는 속성들은 접근할 수 없게끔 제한했다.

참고
타입스크립트 핸드북-제네릭

0개의 댓글