Typescript 개념잡기(7)

오형근·2022년 5월 22일
0

Typescript

목록 보기
7/15
post-thumbnail

제네릭, 타입스크립트에서 가장 재밌는 기능이라고 생각되는 기능에 대해 공부해보자.

타입스크립트 핸드북(타입스크립트 핸드북)을 보고 공부한 것을 정리한 글입니다.
기본적인 JS 지식은 가진 상태라고 가정합니다.


제네릭은 본래 C#, java 등의 언어에서 재사용성이 높은 컴포넌트를 만들 때 사용되는 기능이다. 특히, 한 가지 타입보다 여러 가지 타입에서 동작하는 컴포넌트 제작에 유용하다.

제네릭의 한 줄 정의와 예시

제네릭은 타입을 마치 함수의 파라미터처럼 사용하는 것을 의미한다. 다음 예시를 살펴보자.

function getText(text) {
  return text;
}

위 함수는 text라는 파라미터에 값을 넘겨 받아 text를 반환한다. 이는 누구나 아는 기능이다.

이러한 관점에서 제네릭을 살펴보자. 제네릭은 위와 비슷하지만 파라미터에 타입이 들어가는 것이라고 생각하면 좋다.

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

위 함수는 제네릭 기본 문법이 적용된 상태이다. 이제 함수를 호출할 때 아래와 같이 함수 안에서 사용할 타입을 넘겨줄 수 있다.

getText<string>("hi");
getText<number>(10);
getText<boolean>(true);

위 코드 중 getText<string>("hi")를 호출 했을 때 함수에서 제네릭이 어떻게 동작하는지 살펴보자.

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

위 함수에서 제네릭 타입이 <string>이 되는 이유는 getText() 함수를 호출할 때 제네릭값으로 string을 넘겼기 때문이다.

즉 위의 코드는 다음과 같이 해석되는 것이다.

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

이것이 제네릭의 기능이다! 공용할 수 있는 타입을 지정해준다니...엣지있고 멋있는 기능이 아닐 수 없다.

제네릭을 사용하는 이유

???: 제네릭을 사용하지 않아도 any 타입으로 타입을 열어두면 안되나요?

라고 말하면 바보이다. 문제가 있으니 제네릭이 나온 것 아닐까요?

any를 사용한다고 함수의 동작에 문제가 생기는 것은 아니다. 다만, 함수의 인자로 어떤 타입이 들어갔고 어떤 값이 반환되는지는 알 수가 없다. 왜냐하면 any는 타입 검사를 하지 않으니까!!

이러한 문제점을 해결하기 위해 만들어진 것이 제네릭이다. 제네릭을 사용하면 함수를 호출할 때 넘긴 타입에 대해 타입스크립트가 추정할 수 있게 된다. 따라서, 함수의 입력 값에 대한 타입과 출력 값에 대한 타입이 동일한지 검증할 수 있게 된다.

제네릭 타입 변수

위의 내용으로 제넬기을 사용하기 시작하면 컴파일러에서 이자에 타입을 넣어달라는 경고를 보게 된다.

위의 코드를 다시 보자.

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

만이 여기서 함수의 인자로 받은 값의 length를 확인하고 싶다면 어떻게 해야 할까? 아래와 같이 코드를 작성해야 한다.

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

위 코드를 변환하려고 하면 컴파일러에서 에러를 발생시킨다. 왜냐하면 text.length가 있다는 단서가 없기 때문이다(길이를 가지는 값임을 명시해주어야 한다)

이러한 경우, 아래와 같이 제네릭에 타입을 줄 수 있다.

function logText<T>(text: T[]): T[] {
  console.log(text.length); // 제네릭 타입이 배열이기 때문에 `length`를 허용한다.
  return text;
}

위의 코드가 기존과 다른 것은 인자의 T[]부분이다.
이 제네릭 함수 코드는 우선 T라는 변수 타입을 받고, 인자 값으로는 T로 이루어진 배열을 받는다.

혹은 다음과 같이 좀 더 명시적으로 제네릭 타입을 선언할 수 있다.

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

제네릭 타입

제네릭 인터페이스에 대해 알아보자.

아래의 두 코드는 같은 의미이다.

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

let str: <T>(text: T) => T = logText;

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; // Okay!!!

위 코드에서 만일 인터페이스에 인자 타입을 강조하고 싶다면 아래와 같이 변경이 가능하다.

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

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

let mystring: GenericLogTextFn<string> = logText;

이와 같은 방식으로 제네릭 인터페이스 뿐만 아니라 클래스도 생성할 수 있다. 다만, 이넘과 네임스페이스는 제네릭으로 생성할 수 없다!!!!

제네릭 클래스

제네릭 클래스는 위에서 살펴본 제네릭 인터페이스와 비슷하다. 예시를 살펴보자.

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

let math = new GenericMath<number>();

제네릭 제약 조건

앞에서 제네릭 타입 변수에서 살펴본 내용 말고도 제네릭 함수에 어느 정도 타입 힌트를 줄 수 있는 방법이 있다. 잠시 이전의 코드를 보자.

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

인자의 타입에 선언한 T는 아직 어떤 타입인지 구체적으로 정의하지 않았기 때문에 length 코드에서 오류가 난다. 이럴 때 만약 해당 타입을 정의하지 않고도 length 속성 정도는 허용하려면 아래와 같이 작성한다.

interface LengthWise {
  length number;
}

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

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

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

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

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"); // okay
getProperty(obj, "z"); // error: "z"는 "a", "b", "c" 속성에 해당하지 않습니다.

위와 같이 설정하면 첫 번째 인자로 받는 객체에 없는 속성들은 접근이 불가능하도록 설정 가능하다.


지금까지 제네릭에 대해서 알아보았다. 제네릭은 확장성도 좋고 유용한 기능이므로 꼭 알아두도록 하자.

profile
eng) https://medium.com/@a01091634257

0개의 댓글