티스토리 뷰
▶타입스크립트의 제네릭(Generics)
제네릭은 C#, Java 등의 언어에서 재사용성이 높은 컴포넌트를 만들 때 자주 활용되는 특징입니다.
특히, 한 가지 타입보다 여러 가지 타입에서 동작하는 컴포넌트를 생성하는 데 사용되는데요.
🔎 제네릭의 정의와 예시 - 타입을 함수의 파라미터처럼 사용하자
제네릭이란 타입을 마치 함수의 파라미터처럼 사용하는 것을 의미합니다.
일반적인 자바스크립트의 함수 형태에서 살펴봅시다.
function getText(text) {
return text;
}
위 함수는 text라는 파라미터에 값을 넘겨 받아 text를 반환해 주는 함수입니다.
'hi', 10, true 등 어떤 값이 들어가더라도 그대로 반환합니다.
getText('hi'); // 'hi'
getText(10); // 10
getText(true); // true
이 관점에서 제네릭을 한번 살펴봅시다.
function getText<T>(text: T): T {
return text;
}
위 함수는 제네릭 기본 문법이 적용된 형태이며, 이 함수를 호출할 때 아래와 같이 함수 안에서 사용할 타입을 넘겨줄 수 있습니다.
getText<string>('hi');
getText<number>{3);
getText<boolean>(true);
위 코드 중 getText<string>('hi')를 호출했을 때 함수에서 제네릭이 어떻게 동작하는지 살펴봅시다.
string을 집어 넣었기 때문에 → 인자의 타입과 반환값의 타입 모두 문자열이 된다고 지정한 것과 동일
즉, 제네릭을 사용하면 넘겨준 타입에 따라 인자와 반환값의 타입을 동시에 지정해 줄 수가 있게 됩니다.
🔎 그래서 제네릭은 왜 쓰는거야?
타입스크립트 관련 글에서 몇 번 이야기한 적이 있지만
정확하게 타입을 정의하여 그 타입의 메서드들을 바로 이용할 수 있다는 점은 타입스트립트의 강력한 장점이었습니다.
제네릭을 사용하는 이유로 이와 비슷한데요.
우선, 제네릭을 사용하는 가장 큰 이유는
여러 타입에 대해 사용하기 위해서인데요.
첫 번째, 유지보수면
만약 타입스크립트에서 인자의 타입을 두 개로 받고 싶다면 어떻게 해야 할까요?
우선 아래와 같은 방법도 있겠습니다.
function logText(text: string) {
return text;
}
function logNumber(num: number) {
return num;
}
위 코드와 같은 방법은 로직은 같게 함수를 하나 더 정의하는 방법인데요.
이 방법은 유지보수 면에서 좋지 않은 방법입니다.
왜냐하면 타입 때문에 중복된 함수를 다시 정의하기 때문입니다.
두 번째, 타입 추론
타입을 두 개로 받고 싶을 때, 생각나는 또 다른 방법! 유니온 타입.
function logText(text: string | number) {
return text;
}
위와 같이 함수를 작성하게 되면 문제가 있는데요.
text는 string 타입과 number 타입이 사용할 수 있는 메서드의 교집합 메서드만 제공해 주게 됩니다.
지금까지 말한 문제점들을 제네릭을 통해 해결할 수가 있게 됩니다.
🔎 제네릭 타입 변수
앞에서 배운 내용으로 제네릭을 사용하기 시작한다면 컴파일러에서 인자에 타입을 넣어달라는 경고를 보게 됩니다..
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가 있다는 단서는 어디에도 없기 때문입니다.
다시 위 제네릭 코드의 의미를 살펴보면
함수의 인자와 반환 값에 대한 타입을 정하진 않았지만, 입력 값으로 어떤 타입이 들어왔고 반환 값으로 어떤 타입이 나가는지 알 수 있습니다.
따라서, 함수의 인자와 반환 값 타입에 마치 any를 지정한 것과 같은 동작을 한다는 것을 알 수 있습니다.
이러한 특성 때문에 현재 인자인 text에 문자열이나 배열이 들어와도 아직은 컴파일러 입장에서 .length를 허용할 수 없는 겁니다.
왜나하면 number가 들어왔을 때는 .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
var str: <T>(text: T) => T = logText;
// #2
var 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)는 제네릭으로 생성할 수 없습니다.
🔎 제네릭 클래스
제네릭 클래스는 앞에서 알아본 제네릭 인터페이스와 비슷합니다.
class GenericMath<T> {
pi: T;
sum: (x: T, y: T) => T;
}
let math = new GenericMath<number>();
제네릭 클래스를 선언할 때 클래스 이름 오른쪽에 <T>를 붙여줍니다.
그리고 해당 클래스로 인스턴스를 생성할 때 타입에 어떤 값이 들어갈 지 지정하면 됩니다.
조금 전에 알아본 인터페이스처럼 제네릭 클래스도 클래스 안에 정의된 속성들이 정해진 타입으로 잘 동작하도록 보장할 수 있습니다.
🔎 제네릭 제약 조건
앞에서 제네릭 타입 변수에서 알아본 내용 말고도 제네릭 함수에 어느 정도 타입 힌트를 줄 수 있는 방법이 있습니다.
이전 코드를 다시 볼까요?
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에 대해 동작하는 인자만 넘겨받을 수 있게 됩니다.
logText(10); // Error, 숫자 타입에는 length가 존재하지 않으므로 오류 발생
logText({length: 0, value: 'hi'}) // 'text.length' 코드는 객체의 속성 접근과 같이 동작하므로 오류 없음
함수가 받는 인자를 제한하는 방법
우선 keyof 키워드를 이용하여 함수의 인자값을 제한하는 방법을 알아보겠습니다.
interface ShoppingItem {
name: string;
price: number;
stock: number;
}
// ShoppingItem 인터페이스에 있는 키값들만
// 함수의 인자로 들어갈 수 있음
function getShoppingItemOption<T extends keyof ShoppingItem>(itemOption: T): T {
return itemOption;
}
getShoppingItemOption(10); // Error
getShoppingItemOption('name');
우선 사용할 인터페이스를 하나 정의해 줍니다.
이후에 함수를 정의할 때 'T extends keyof 인터페이스명' 을 같이 작성해 주게 되면
제네릭 때문에 같이 적어준 인터페이스의 키값들로만 인자를 받을 수 있게 됩니다.
따라서 인자로 10을 넘겨주면 에러가 발생하고,
인자로 인터페이스에 정의된 name이라는 키값을 넘겨주게 되면 에러 없이 실행되는 것이죠.
함수를 사용할 떄 들어가는 인자의 값을 제한하고 싶다면 위와 같은 keyof 키워드를 사용하여 제한하는 것도 유용할 거 같습니다.
객체의 속성을 제약하는 방법
두 객체를 비교할 때도 제네릭 제약 조건을 사용할 수 있습니다.
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" 속성에 해당하지 않습니다.
제네릭을 선언할 때 <o extends keyof T> 부분에서 첫 번째 인자로 받는 객체에 없는 속성들은 접근할 수 없게끔 제한하였습니다.
'프론트엔드 > TypeScript' 카테고리의 다른 글
[TypeScript] 타입스크립트 덕 타이핑과 구조적 타이핑 (2) | 2023.05.09 |
---|---|
[TypeScript] 타입스크립트 유틸리티 타입 - 보다 간결하게✨ (0) | 2023.04.14 |
[TypeScript] 타입스크립트 클래스(Class) (0) | 2023.01.03 |
[TypeScript] 자바스크립트 프로토타입(Prototype) - 프로토타입으로 상속하여 객체를 재사용하자 (0) | 2023.01.03 |
[TypeScript] 타입스크립트 이넘(Enums) (0) | 2023.01.02 |
- Total
- Today
- Yesterday
- react-query
- 디프만
- HTML
- 데이터분석
- 프로젝트 회고
- jest
- 딥러닝
- styled-components
- next.js
- 파이썬
- testing
- 인프런
- rtl
- 리액트 훅
- 프론트엔드 기초
- 프론트엔드 공부
- 자바
- JSP
- 머신러닝
- 자바스크립트
- 타입스크립트
- react
- 스타일 컴포넌트 styled-components
- frontend
- 자바스크립트 기초
- 프론트엔드
- 리액트
- CSS
- Python
- TypeScript
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |