제네릭은 C#, JAVA 등의 언어에서 재사용성이 높은 컴포넌트를 만들 때 자주 활용되는 특징이 있다. 특히, 한 가지 타입보다 여러 가지 타입에서 동작하는 컴포넌트를 생성하는 데 사용된다.
✅ 제네릭의 한 줄 정의와 예시
제네릭이란 타입을 마치 함수의 파라미터처럼 사용하는 것을 의미한다.
위 함수는 text라는 파라미터에 값을 넘겨받아 text를 반환해 준다. hi, 10, true 등 어떤 값이 들어가더라도 그대로 반환한다.
function getText(text) {
return text;
}
이 관점에서 제네릭을 한 번 살펴보자. 아래 함수는 제네릭 기본 문법이 적용된 형태다.
이제 함수를 호출할 때 아래와 같이 함수 안에서 사용할 타입을 넘겨줄 수 있다.
function getText<T>(text: T): T {
return text;
}
getText<string>('hi');
getText<number>(10);
getText<boolean>(true);
위 코드 중 첫 번째 함수를 호출했을 때 함수에서 제네릭이 어떻게 동작하는지 살펴보자. 먼저 아래 함수에서 제네릭 타입이 <string>이 되는 이유는 getText() 함수를 호출할 때 제네릭(함수에서 사용할 타입) 값으로 string을 넘겼기 때문이다.
function getText<string>(text: T): T {
return text;
}
그럼, getText 함수는 아래와 같이 타입을 정의한 것과 같다.
아래 함수는 입력 값의 타입이 string이면서 반환 값 타입도 string이어야 한다.
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라는 타입은 타입 검사를 하지 않기 때문이다.
이러한 문제점을 해결할 수 있는 것이 제네릭이다.
먼저 함수의 이름 바로 뒤에 <T>라는 코드를 추가했다. 그리고 함수의 인자와 반환 값에 모두 T라는 타입을 추가한다. 이렇게 되면 함수를 호출할 때 넘긴 타입에 대해 타입스크립트가 추정할 수 있게 된다. 따라서, 함수의 입력 값에 대한 타입과 출력 값에 대한 타입이 동일한지 검증할 수 있게 된다.
function logText<T>(text: T): T {
return text;
}
그리고 이렇게 선언한 함수는 아래와 같이 2가지 방법으로 호출할 수 있다. 보통 두 번째 방법이 코드도 더 짧고 가독성이 좋아서 흔히 사용된다. 그렇지만 만약 복잡한 코드에서 두번째 코드로 타입 추정이 되지 않는다면 첫 번째 방법을 사용하면 된다.
// #1
const text = logText<string>("Hello Generic");
// #2
const text = logText("Hello Generic");
✅ 제네릭 타입 변수
앞에서 배운 내용으로 제네릭을 사용하기 시작하면 컴파일러에서 인자에 타입을 넣어달라는 경고를 보게 된다.
function logText<T>(text: T): T {
return text;
}
만약 여기서 함수의 인자로 받은 값의 length를 확인하고 싶다면 아마 아래와 같이 코드를 작성할 것이다. 그러나 아래 코드를 변환하려고 하면 컴파일러에서 에러를 발생시킨다. 왜냐하면 text에 .length가 있다는 단서는 어디에도 없기 때문이다.
function logText<T>(text: T): T {
console.log(text.length); // Error: T doesn't have .length
return text;
}
다시 제네릭 코드의 의미를 살펴보면 함수의 인자와 반환 값에 대한 타입을 정하진 않았지만, 입력 값으로 어떤 타입이 들어왔고 반환 값으로 어떤 타입이 나가는지 알 수 있다. 따라서 함수의 인자와 반환 값 타입에 마치 any를 지정한 것과 같은 동작을 한다는 것을 알 수 있다. 그래서 설령 인자에 nubmer 타입을 넘기더라도 에러가 나진 않는다. 이러한 특성 때문에 현재 인자인 text에 문자열이나 배열이 들어와도 아직은 컴파일러 입장에서 .length를 허용할 수 없다. 왜냐하면 nubmer가 들어왔을 때 .length 코드가 유효하지 않기 때문이다.
그래서 이런 경우에는 아래와 같이 제네릭에 타입을 줄 수 있다.
function logText<T>(text: T[]): T[] {
console.log(text.length); // 제네릭 타입이 배열이기 때문에 `length`를 허용한다.
return text;
}
위 코드가 기존의 제네릭 코드와 다른 점은 T[ ] 부분이다. 이 제네릭 함수 코드는 일단 T라는 변수 타입을 받고, 인자 값으로는 배열 형태의 T를 받는다. 예를 들면, 함수에 [1, 2, 3]처럼 숫자로 이뤄진 배열을 받으면 반환 값으로 number를 돌려준다. 이런 방식으로 제네릭을 사용하면 꽤 유연한 방식으로 함수의 타입을 정의해 줄 수 있다.
혹은 배열 형태일 경우, 아래와 같이 좀 더 명시적으로 제네릭 타입을 선언할 수 있다.
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; // Okay
위 코드에서 만약 인터페이스에 인자 타입을 강조하고 싶다면 아래와 같이 변경할 수 있다.
interface GenericLogTextFn<T> {
(text: T): T;
}
function logText<T>(text: T): T {
return text;
}
let myString: GenericLogTextFn<string> = logText;
이와 같은 방식으로 제네릭 인터페이스뿐만 아니라 클래스도 생성할 수 있다. 다만, 이넘(enum)과 네임스페이스(namespace)는 제네릭으로 생성할 수 없다.
'🍞 Front-End > TypeScript' 카테고리의 다른 글
[TS] type과 interface 차이 (2) | 2023.02.01 |
---|---|
[React/TS] 자주겪는 문제 해결방법 (0) | 2022.12.16 |
[TS] 타입스크립트란? (TypeScript) (0) | 2022.12.14 |