Today Sangmin Learned
article thumbnail
Published 2021. 6. 23. 13:25
[TypeScript] 제네릭(Generic) Web
728x90
이 글은 인프런의 TypeScript 입문 - 기초부터 실전까지 강의를 듣고 정리한 글입니다.

제네릭이란, 타입을 마치 함수의 파라미터처럼 사용하는 것을 의미한다. 아래 코드를 보자.

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

logText에서 text에 대해 T로 타입을 설정해줬지만, 정작 이 T가 무엇인지에 대해서는 아는 바가 없다.

실제로 프리뷰를 보면, <"hi">(text: "hi"): "hi" 로 되어있는 것을 볼 수 있다. 그래서, 함수를 불러올 때 타입을 지정해줘야 한다.

logText에 명시적으로 string 타입을 넘기겠다고 선언했으므로, logText 안에서 처리하는 텍스트에 대한 타입은 전부 위에서 넘겼던 문자열이라고 볼 수 있다.

 

이 제네릭을 왜 쓸까? 이는, 기존 타입 정의 방식과 문법에서의 단점이 존재하기 때문이다.

1. 함수 중복 선언

function logText(text: string) {
  console.log(text);
  return text;
}

function logNumber(num: number) {
  console.log(num);
  return num;
}

logText("a");
const num = logNumber(10);

string과 number 타입을 받기 위해 역할이 똑같은 (콘솔에 출력한 뒤 return)함수를 두 개를 만들게 된다. -> input에 대한 문제점이 발생한다. -> 이를 해결하는 방법은, 유니온 타입으로 선언하는 것이다.

2. 유니온 타입을 이용한 선언 방식의 문제점

function logText(text: string | number) {
  console.log(text);
  return text;
}

const a = logText("a");
a.split('');

이제 text에 string | number로 유니온 타입 형태로 타이핑을 해주었다. 이 경우에는, 아래 두 줄을 보면 문제점을 확인할 수 있다.

현재 logText 함수 내에서 text를 쳐 보면, 이렇게 string과 number가 모두 타이핑이 되어있는데,

const a = logText('a');로, string type을 사용할 것임을 확인할 수 있다. 문제는 문자열 속성인 split을 사용하려고 하니까 에러가 뜬다는 점이다. 즉 유니온 타입을 사용할 경우 반환값에 대한 문제점이 생긴 것이다.

 

이러한 두가지 문제점

(기존 타입 방식: 함수 중복 선언(input 단에서의 문제) & 유니온 타입 방식: 명시하였음에도 속성 사용 불가(반환값에 대한 문제점))

을 제네릭으로 해결할 수 있다.

 

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

const str = logText<string>("abc");
str.split("");
const login = logText<boolean>(true);

위에서 정의한 대로, logText<T>(text: T): T로, 어떤 타입을 받을 건지 미리 정의를 하였다. 이 꺾쇠 형태는 제네릭을 사용하겠다는 것으로 생각해도 무방하다.

 

이제 const str = logText<string>('abc'); 를 보면, logText라는 함수를 사용하는 그 시점에 typing을 해 준다. 그래서,

logText의 파라미터인 text에 대해서도 string typing이 잘 되어 있고, 반환값 또한 T였기 때문에 string typing이 잘 되어 있는 것을 확인할 수 있다. 그래서, 문자열 속성인 split도 에러 없이 사용가능하다.

뿐만아니라, 함수를 사용하는 그 시점에 타이핑을 해주는 것이기 때문에 string이나 number 뿐만아니라 맨 아랫줄과 같이 boolean으로도 사용할 수 있다.

 

인터페이스에 제네릭 선언하기

인터페이스에 제네릭을 선언하는 방법도 크게 다르지 않다.

// 제너릭 선언 X
interface Dropdown {
  value: string,
  selected: boolean;
}

const obj: Dropdown {
  value: 10, // 'number' 형식은 'string' 형식에 할당할 수 없습니다.ts(2322)
  selected: false,
}

// 제네릭 선언 O
interface Dropdown<T> {
  value: T;
  selected: boolean;
}

const obj: Dropdown<number> {
  value: 10,
  selected: false,
}

인터페이스를 선언할 때 <T>로 제네릭을 사용할 것임을 명시하고, 이 Dropdown 인터페이스를 사용하는 시점에 <타입>으로 정의해주면 된다. 제네릭을 선언하지 않은 경우는 당연히 오류가 난다. 이 부분은 따로 설명하지 않아도 될 것 같으므로 넘어가고,

제네릭을 선언한 경우에는 obj 변수에 대해 Dropdown 인터페이스를 사용할 때 <number>로, T에 대해서는 number typing을 적용하도록 하여 value에 숫자가 들어가도 오류가 생기지 않는다.

 

제네릭의 타입 제한 1: 파라미터 변경

function logTextLength<T>(text: T): T {
  console.log(text.length);
  return text;
}
logTextLength('hi');

이렇게 코드를 짜 보면, length 부분에 'T' 형식에 'length' 속성이 없습니다.ts(2339) 라는 에러를 낸다.

아래에 'hi'라는 문자열을 넣어줬기 때문에, length를 사용할 수 있을 것 같았지만, 현재 타입스크립트에서는 이 logTextLength가 어떤 타입을 가질 지 알 수가 없다. T에 해당하는 선언을 해주지 않았기 때문이다.

이 경우 length가 나오게 하려면, 타입 제한을 줘야 한다.

function logTextLength<T>(text: T[]): T[] {
  console.log(text.length);
  text.forEach(function(text) {
    console.log(text);
  });
  return text;
}
logTextLength<string>(['hi', 'abc', 'def']);

text에 대해 T를 배열로 줬다. 이렇게 된다면 배열 안에 있는 것들을 문자열이라고 인식을 하기 때문에 length도 사용할 수 있다. 다만, 이 경우에 맨 아랫줄처럼 <string>으로 지정을 해줬더라도 오류는 생기는데, 괄호 안에 문자열이 아니라 배열의 형태로 들어가야 하기 때문이다. 그래서, 원하는 텍스트를 출력하려면 우선 파라미터에 배열로 넣어준 뒤에 함수 안에서 forEach 반복문을 돌려서 해당하는 텍스트를 출력하게끔 해야 한다.

제네릭의 타입 제한 2: 정의된 타입 이용

interface LengthType {
  length: number;
}
function logTextLength<T extends LengthType>(text: T): T {
  text.length;
  return text;
}
logTextLength("abc"); // 가능
logTextLength(10); // '10' 형식의 인수는 'LengthType' 형식의 매개 변수에 할당될 수 없습니다.ts(2345)
logTextLength({ length: 10 });

미리 LengthType이라는 인터페이스를 만든 뒤 logText 함수의 꺾쇠에 LengthType을 받아오는 T를 지정함으로써 이제 length 형태를 갖고 있는 T라면(예를 들면 문자열) 그것을 가져올 수 있게끔 한 것이다.

logTextLength(10)은, 숫자(10)에는 length가 제공되지 않기 때문에 오류가 생기게 된다.

제네릭의 타입 제한 3: keyof 이용

interface ShoppingItem {
  name: string;
  price: number;
  stock: number;
}

function getShoppingItemOption<T extends keyof ShoppingItem>(itemOption: T): T {
  return itemOption;
}
getShoppingItemOption(10); //'10' 형식의 인수는 '"name" | "price" | "stock"' 형식의 매개 변수에 할당될 수 없습니다.ts(2345)
getShoppingItemOption<string>('a'); // 'string' 형식이 '"name" | "price" | "stock"' 제약 조건을 만족하지 않습니다.ts(2344)

getShoppingItemOption('name'); // OK

function의 꺾쇠 부분을 보자. T extends keyof ShoppingItem이란, ShoppingItem interface의 key값들(name, price, stock) 중 하나가 T(제네릭)가 된다는 것이다. 결론적으로 ShoppingItemOption의 파라미터로는 name, price, stock 중 한 가지만 들어갈 수 있다는 것이다.

 

근데 이렇게 할 경우, 아래 두 줄에 모두 에러가 뜬다. 그 이유를 보면

1) 10은 우리가 받을 수 있는 타입(name, price, stock)에 해당하지 않기 때문이다.

2) 꺾쇠 자체가 필요가 없기 때문이다. (왜냐면 string 또는 number로 이미 interface에서 타입 정의를 해준 상황이기 때문. 그냥 'name', 'price', 'stock' 중 하나만 들어갈 수 있다고 생각하면 편함)

 

따라서, 맨 아랫줄과 같이 파라미터에 key들을 넣어주는 방식으로 해야 한다.

profile

Today Sangmin Learned

@steadily-worked

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!