제네릭(Generic)은 타입을 마치 함수의 매개변수처럼 사용하는 기능이다. 이를 통해 코드의 재사용성을 높이고, 타입의 안정성을 유지할 수 있다.
일반적으로 타입을 지정하면 특정 타입의 값만 받을 수 있지만, 제네릭을 사용하면 다양한 타입을 처리할 수 있는 유연한 코드 작성이 가능하다.
다음과 같은 함수들이 있다고 가정해 본다면,
function printStrings(arr: string[]): void {
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
}
function printNumbers(arr: number[]): void {
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
}
위 두 함수는 역할이 동일하지만, string[]과 number[]만 처리할 수 있다. 만약 boolean[]을 처리하는 함수가 필요하면 새로운 함수를 또 만들어야 한다. 중복 코드가 많아지는 단점이 있다.
제네릭을 사용하면 다음과 같이 하나의 함수로 여러 타입을 처리할 수 있다.
function printAnything<T>(arr: T[]): void {
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
}
제네릭 <T>를 사용하여 T[] 타입을 받도록 하면 string[], number[], boolean[] 등 다양한 타입을 처리할 수 있다.
printAnything(['a', 'b', 'c']); // T는 string으로 추론
printAnything([1, 2, 3]); // T는 number로 추론
위와 같이 타입을 명시적으로 지정하지 않아도 TypeScript가 자동으로 타입을 추론한다.
React의 useState에서도 제네릭을 사용할 수 있다.
import { useState } from "react";
function App() {
const [counter, setCounter] = useState<number>(1);
const increment = () => {
setCounter((prev) => prev + 1);
};
return <div onClick={increment}>{counter}</div>;
}
export default App;
useState<number>(1)과 같이 제네릭을 사용하면 counter의 타입이 number로 지정되며, setCounter도 number 값을 받아야 한다.
그러나 TypeScript는 초기값 1이 number임을 자동으로 추론하기 때문에, 다음과 같이 작성해도 동일한 결과가 나온다.
const [counter, setCounter] = useState(1);
제네릭을 사용하지 않아도 타입이 자동으로 지정되지만, 경우에 따라 명시적으로 제네릭을 지정하는 것이 가독성을 높이는 데 도움이 될 수 있다.
TypeScript는 제네릭 타입도 자동으로 추론할 수 있다.
function identity<T>(arg: T): T {
return arg;
}
let output = identity("Hello"); // T는 string으로 추론
identity("Hello")를 호출하면 T가 string으로 추론되어 identity<string>(arg: string): string으로 동작한다.
제네릭을 사용하여 인터페이스를 정의할 수 있다.
interface Box<T> {
value: T;
}
const numberBox: Box<number> = { value: 42 };
const stringBox: Box<string> = { value: "Hello" };
제네릭을 여러 개 사용할 수도 있다.
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
const mergedObj = merge({ name: "Alice" }, { age: 25 });
merge 함수는 두 개의 다른 타입을 받아 합친 후 반환한다.
제네릭을 클래스에서도 사용할 수 있다.
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() {
return [...this.data];
}
}
const textStorage = new DataStorage<string>();
textStorage.addItem("Hello");
textStorage.addItem("World");
textStorage.removeItem("Hello");
console.log(textStorage.getItems()); // ["World"]
제네릭을 사용하면 타입을 유연하게 처리할 수 있으며, 코드의 재사용성을 높일 수 있다. 함수, 인터페이스, 클래스 등 다양한 곳에서 활용할 수 있으며, TypeScript의 타입 안정성을 유지하면서도 범용적인 코드를 작성할 수 있도록 도와준다.