의도: 제네릭에 대한 지식을 가지고 있는지 확인하는 질문
팁: 쉬운 예시를 들어도 좋다.
나의 답안
네, 사용해 봤습니다.
제네릭은 함수나 컴포넌트가 다양한 타입의 값을 처리할 수 있도록 하면서도, 타입 안정성을 유지하는 기능이라고 생각합니다.제가 진행한 프로젝트에서는 공통 페이지네이션 응답을
ApiResponse<T>라는 제네릭으로 한 번만 정의해 두고, 도메인에 따라 콘텐츠 타입만 교체해서 재사용했습니다.이렇게 하면 페이지 번호, 정렬 정보 등 응답의 공통 필드는 전부 한 타입에서 일관되게 관리하고, 실제 데이터 구조만 제네릭 매개변수로 바꾸면 되기 때문에 타입 안정성과 재사용성이 높아집니다.
주어진 답안 (모범 답안)
네, 제네릭을 사용하여 여러 코드에 걸쳐 일관성을 높여 재사용성과 유지 보수성, 안정성을 향상시킨 경험이 있습니다.
제네릭을 사용한다면 함수 내부의 타입들이 마치 톱니바퀴가 움직이듯 척척 맞물리게 되어, 비록 작성이 어려울지언정 결과물에 대해서는 꽤나 뿌듯했습니다.그리고 제네릭이라는 게 사실 리액트의
useState에서 객체의 상태를 다룰 때 필요했던 거라 생각보다 쉽게 마주할 수 있어 그렇게 낯선 개념도 아니었습니다.
개인적으로는 C나 Java에서 봤던 개념이라 사용에 익숙했을지도 모른다고 생각합니다.
타입스크립트는 기본적으로 정적 타입을 제공하지만, 재사용성을 높이면서도 타입 안전성을 유지하려면 제네릭이 필요하다.
제네릭이 없는 경우
다음과 같은 identity 함수가 있다고 가정하자.
function identity(arg: any): any {
return arg;
}
const result = identity(10);
console.log(result.toUpperCase()); // [에러] 런타임 에러 발생
any 타입을 사용하면 모든 타입을 받을 수 있지만, 타입 안전성이 보장되지 않는다.result가 숫자형(number)인데 toUpperCase()를 호출하면 런타임 에러가 발생한다.제네릭을 사용하면 함수나 클래스가 특정 타입에 종속되지 않고 유연한 타입을 가질 수 있다.
제네릭 함수
제네릭을 사용하여 identity 함수를 개선하면 다음과 같다.
function identity<T>(arg: T): T {
return arg;
}
// 사용 예시
const num = identity<number>(10);
const str = identity<string>("Hello");
console.log(num.toFixed(2)); // 타입 안전
console.log(str.toUpperCase()); // 타입 안전
<T>는 타입 매개변수(Type Parameter)이며, 함수가 호출될 때 T가 결정된다.identity<number>(10)을 호출하면 T = number가 되고, identity<string>("Hello")를 호출하면 T = string이 된다.제네릭 인터페이스
제네릭을 사용하여 다양한 타입의 데이터를 저장하는 인터페이스를 만들 수 있다.
interface Box<T> {
value: T;
}
// number 타입을 지정하는 Box
const numberBox: Box<number> = { value: 42 };
// string 타입을 지정하는 Box
const stringBox: Box<string> = { value: "Hello" };
console.log(numberBox.value); // 42
console.log(stringBox.value.toUpperCase()); // "HELLO"
Box<T>는 T 타입의 value를 가진다.numberBox는 number, stringBox 타입은 string 타입을 가지도록 제한할 수 있다.제네릭 클래스
제네릭을 활용하면 클래스도 타입에 관계없이 재사용할 수 있다.
class DataStorage<t> {
private data: T[] = [];
addItem(item: T) {
this.data.push(item);
}
removeItem(item: T) {
this.data = this.data.filter(i => i !== item);
}
getItems(): T[] {
return this.data;
}
}
// string 타입을 저장하는 DataStorage
const textStorage = new DataStorage<string>();
textStorage.addItem("Hello");
textStorage.addItem("World");
textStorage.removeItem("Hello");
console.log(textStorage.getItems()); // ["World"]
// number 타입을 저장하는 DataStorage
const numberStorage = new DataStorage<number>();
numberStorage.addItem(10);
numberStorage.addItem(20);
console.log(numberStorage.getItems()); // [10, 20]
T를 사용하여 DataStorage가 여러 타입을 지원하도록 만들었다.textStorage는 string 타입만 저장할 수 있으며, numberStorage는 number 타입만 저장할 수 있다.제네릭 타입 제한(constraints)
제네릭을 사용할 때는 모든 타입을 허용할 수도 있지만, 특정 타입만 받도록 제한할 수도 있다.
예를 들어, length 속성을 가진 타입만 허용하도록 제한하려면 extends 키워드를 사용한다.
// T는 반드시 length 속성을 가져야 한다.
function logLength<T extends { length: number }>(arg: T): void {
console.log(arg.length);
}
// 문자열(string)은 length 속성이 있음
logLength("Hello"); // 가능
// 배열(array)도 length 속성이 있음
logLength([1, 2, 3]); // 가능
// 객체(objects)도 length 속성이 있으면 가능
logLength({ length: 10, name: "Object" }); // 가능
// 숫자(number)는 length 속성이 없음
logLength(100); // [에러] 불가능
T extends { length: number }를 사용하여 length 속성이 있는 타입만 허용했다.{ length: number }가 있는 객체는 허용되지만, number는 허용되지 않는다.제네릭 키 타입 (keyof 활용)
제네릭과 keyof를 함께 사용하면 객체의 속성을 동적으로 가져올 수 있다.
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
const person = { name: "Alice", age: 25 };
console.log(getProperty(person, "name")); // "Alice"
console.log(getProperty(person, "age")); // 25
console.log(getProperty(person, "height")); // [오류] height는 존재하지 않음
keyof는 객체 타입의 키(key)들을 문자열 리터럴 타입으로 추출하는 연산자이다.K extends keyof T를 사용하여 K가 T 객체의 속성 중 하나만 허용되도록 제한했다.height)을 가져오려 하면 오류가 발생한다.API 응답 타입 정의
제네릭을 활용하면 API 응답 타입을 유연하게 정의할 수 있다.
interface ApiResponse<T> {
status: number;
message: string;
data: T;
}
// 사용자 데이터 API 응답
const userResponse: ApiResponse<{ id: number; name: string }> = {
status: 200,
message: "Success",
data: { id: 1, name: "Kim" },
};
// 상품 데이터 API 응답
const productResponse: ApiResponse<{ id: number; price: number }> = {
status: 200,
message: "Success",
data: { id: 101, price: 3000 },
};
ApiResponse<T>를 사용하면 다양한 데이터 타입을 처리할 수 있다.userResponse는 사용자 데이터를, productResponse는 상품 데이터를 처리한다.keyof, API 응답 타입 등 다양한 활용 방법이 존재한다.