제네릭(Generic)
질문
TypeScript의 제네릭(Generic)은 무엇이며, 왜 사용하는 건가요?
답변 초안
제네릭(Generic)은 타입을 값처럼 매개변수화(parameterize)하는 기능입니다.
함수나 클래스, 인터페이스를 작성할 때 타입을 미리 고정하지 않고, 사용하는 시점에 타입을 전달받아 재사용할 수 있게 해줍니다.
이를 통해 any처럼 타입 검사를 포기하지 않으면서도 다양한 타입을 처리할 수 있습니다.
한 줄 비유 제네릭은 "빈 틀(template)"입니다. 붕어빵 틀 하나로 팥 붕어빵도 만들고 슈크림 붕어빵도 만들 수 있듯이, 같은 코드를 여러 타입에 대해 재사용할 수 있습니다.
제네릭이 필요한 이유
타입을 그대로 반환하는 함수를 만들어 보겠습니다.
function identity(value: string): string {
return value;
}
이 함수는 문자열만 처리할 수 있습니다.
숫자도 처리하려면 별도의 함수를 만들어야 합니다.
function identityString(value: string): string {
return value;
}
function identityNumber(value: number): number {
return value;
}
하지만 함수 로직은 완전히 동일합니다.
타입만 다를 뿐입니다.
이를 해결하기 위해 any를 사용할 수도 있습니다.
function identity(value: any): any {
return value;
}
하지만 any는 타입 검사를 포기합니다.
const result = identity('hello');
result.toFixed(); // 컴파일 통과, 런타임 에러
제네릭은 타입 안정성을 유지하면서 재사용성을 제공합니다.
function identity<T>(value: T): T {
return value;
}
const str = identity('hello'); // string
const num = identity(123); // number
TypeScript가 전달된 값을 보고 T를 자동 추론합니다.
제네릭 문법
제네릭 타입 매개변수는 꺾쇠 괄호(< >) 안에 선언합니다.
function identity<T>(value: T): T {
return value;
}
여기서 T는 특별한 의미가 있는 키워드가 아닙니다.
관례적으로 Type을 의미하는 T를 사용하지만 원하는 이름을 사용할 수 있습니다.
function identity<ValueType>(value: ValueType): ValueType {
return value;
}
타입 추론(Type Inference)
대부분의 경우 타입 인수를 직접 적지 않아도 됩니다.
function identity<T>(value: T): T {
return value;
}
const str = identity('hello');
컴파일러는 전달된 인수로부터 T가 string임을 추론합니다.
물론 명시적으로 작성할 수도 있습니다.
const str = identity<string>('hello');
둘은 동일하게 동작합니다.
여러 개의 타입 매개변수
제네릭은 여러 타입을 동시에 받을 수도 있습니다.
function pair<T, U>(first: T, second: U) {
return [first, second];
}
const result = pair('hello', 123);
// [string, number]
실제로 React Query나 Axios 등의 라이브러리에서도 여러 개의 제네릭을 자주 사용합니다.
Promise<User>;
Map<string, User>;
제네릭 제약(Constraint)
때로는 모든 타입을 허용하면 안 되는 경우가 있습니다.
예를 들어 length 프로퍼티를 사용하는 함수는 길이를 가진 타입만 받아야 합니다.
function printLength<T>(value: T) {
console.log(value.length);
}
에러가 발생합니다.
printLength(123);
number에는 length가 없기 때문입니다.
이때 extends를 사용해 제약을 걸 수 있습니다.
interface HasLength {
length: number;
}
function printLength<T extends HasLength>(value: T) {
console.log(value.length);
}
printLength('hello');
printLength([1, 2, 3]);
printLength(123); // 에러
extends는 "상속"이라기보다는
"이 조건을 만족하는 타입만 허용한다"
에 가깝게 이해하면 됩니다.
keyof와 함께 사용하기
제네릭은 keyof와 함께 자주 사용됩니다.
function getProperty<T, K extends keyof T>(
obj: T,
key: K
) {
return obj[key];
}
const user = {
name: 'Kim',
age: 20,
};
getProperty(user, 'name');
존재하지 않는 키는 컴파일 단계에서 막을 수 있습니다.
getProperty(user, 'email');
// 에러
이 패턴은 TypeScript의 대표적인 제네릭 활용 예시입니다.
제네릭 인터페이스
인터페이스에도 제네릭을 적용할 수 있습니다.
interface ApiResponse<T> {
data: T;
success: boolean;
}
interface User {
id: number;
name: string;
}
const response: ApiResponse<User> = {
data: {
id: 1,
name: 'Kim',
},
success: true,
};
실무에서는 API 응답 타입을 정의할 때 매우 자주 사용됩니다.
제네릭 클래스
클래스도 타입을 매개변수로 받을 수 있습니다.
class Storage<T> {
private data: T[] = [];
add(item: T) {
this.data.push(item);
}
getAll() {
return this.data;
}
}
const stringStorage = new Storage<string>();
stringStorage.add('hello');
stringStorage.add('world');
이제 Storage<string>에는 문자열만 저장할 수 있습니다.
실무에서 가장 많이 보는 제네릭
Promise
Promise<User>
Promise<T>에서 T는 미래에 resolve될 값의 타입입니다.
const user = await fetchUser();
// User 타입
Array
Array<string>
사실 아래 문법과 동일합니다.
string[]
React useState
const [user, setUser] = useState<User | null>(null);
User | null이 제네릭 인수로 전달된 것입니다.
제네릭과 any의 차이
많은 사람들이 제네릭을 any와 혼동합니다.
function identity(value: any): any {
return value;
}
any는 타입 정보를 잃어버립니다.
const result = identity('hello');
// any
result.toFixed();
반면 제네릭은 타입 정보를 유지합니다.
function identity<T>(value: T): T {
return value;
}
const result = identity('hello');
// string
result.toUpperCase();
즉,
any→ 타입 검사 포기Generic→ 타입을 전달받아 유지
라는 차이가 있습니다.
실무 주의점
- 제네릭을 남용하면 오히려 타입이 복잡해질 수 있습니다.
- 단순히 타입을 하나 더 감싸기만 하는 제네릭은 불필요할 수 있습니다.
T,U,K,V같은 이름은 관례일 뿐이며 의미 있는 이름을 사용하는 것이 가독성에 도움이 되는 경우도 있습니다.- 제네릭을 사용할 때는 "정말 타입을 재사용해야 하는가?"를 먼저 고민하는 것이 좋습니다.
면접 답변
"제네릭은 타입을 매개변수처럼 전달받아 재사용할 수 있게 해주는 기능입니다. any와 달리 타입 정보를 유지하기 때문에 타입 안정성을 확보하면서도 다양한 타입을 처리할 수 있습니다. 함수, 인터페이스, 클래스에서 사용할 수 있으며, 실무에서는 Promise<T>, Array<T>, React의 useState<T> 같은 형태로 자주 사용됩니다. 또한 extends, keyof와 조합해 타입 제약을 걸거나 안전한 프로퍼티 접근을 구현할 수 있습니다."