안녕하세요, 웹 OB 김채은입니다 -!
타입스크립트를 사용해 개발을 하다보면, 어느 순간 제네릭(Generic)이라는 문법을 접하게 되는데요..! 처음에는 any와 비슷해 보일 수 있지만, 실제로는 타입 추론과 코드 재사용성 면에서 훨씬 강력합니다. 이 글에서는 책 "우아한 타입스크립트 with 리액트"의 내용을 바탕으로 제네릭의 개념, 사용 방법, 그리고 주의할 점을 정리해보려 합니다.
📌 제네릭이란?
제네릭(Generic)은 C, Java 같은 정적 타입 언어에서 다양한 타입에 대해 동일한 코드를 재사용할 수 있도록 고안된 문법입니다. 타입스크립트도 정적 타입 언어이기 때문에 제네릭을 지원합니다.
예를 들어 다음과 같이 타입을 변수처럼 취급할 수 있어요:
function exampleFunc<T>(arg: T): T[] {
return new Array(3).fill(arg);
}
exampleFunc("hello"); // T는 string으로 추론됨
여기서 T는 타입 변수로, 사용할 때 외부에서 지정해주는 타입입니다. T 외에도 E, K, V 등 한 글자 약어가 자주 쓰입니다.
🆚 any와의 차이
any는 타입 체크를 하지 않고 모든 타입을 허용하지만, 제네릭은 특정 타입으로 한정하면서도 다양한 타입을 다룰 수 있게 해줍니다.
type ExampleArrayType<T> = T[];
const array1: ExampleArrayType<string> = ["치킨", "피자", "우동"];
// any를 사용할 경우
const array2: any[] = [
"치킨",
{ id: 0, name: "치킨", price: 20000, quantity: 1 },
99,
true,
];
any는 타입 정보를 잃어버리지만, 제네릭은 배열 요소들이 동일한 타입임을 보장합니다.
✅ 타입 추론 및 기본값
제네릭 함수 호출 시 타입을 명시하지 않아도, 타입스크립트가 인자를 통해 추론합니다.
function exampleFunc<T>(arg: T): T[] {
return new Array(3).fill(arg);
}
exampleFunc("hello"); // string으로 추론
필요할 경우 기본값도 지정할 수 있어요:
interface SubmitEvent<T = HTMLElement> extends SyntheticEvent<T> {
submitter: T;
}
📛 제약 조건 (extends)
모든 타입이 특정 속성을 가진 것은 아니기 때문에 제약을 걸어줄 수 있습니다:
function exampleFunc2<T extends { length: number }>(arg: T): number {
return arg.length;
}
이렇게 하면 length 속성을 가진 타입만 사용할 수 있게 됩니다.
🧨 TSX 환경에서의 제네릭
파일 확장자가 .tsx일 때는 화살표 함수에서 제네릭을 사용할 경우 JSX의 태그 문법과 혼동되어 오류가 날 수 있는데요:
// 오류 발생
const arrowExampleFunc = <T>(arg: T): T[] => {
return new Array(3).fill(arg);
};
// 오류 회피 (extends 사용)
const arrowExampleFunc2 = <T extends {}>(arg: T): T[] => {
return new Array(3).fill(arg);
};
보통 이럴 땐 function 키워드를 사용한 선언형 함수로 대체하는 것이 좋습니다.
📦 실전 예시: API 응답 타입
export interface MobileApiResponse<Data> {
data: Data;
statusCode: string;
statusMessage?: string;
}
export const fetchPriceInfo = (): Promise<MobileApiResponse<PriceInfo>> => {
return request({ method: "GET", url: "/price" });
};
export const fetchOrderInfo = (): Promise<MobileApiResponse<Order>> => {
return request({ method: "GET", url: "/order" });
};
이처럼 다양한 API 응답에서 반복되는 타입을 제네릭으로 추상화하면, 코드의 재사용성과 가독성이 대폭 향상됩니다 ! !
🧱 클래스에서의 제네릭
class LocalDB<T> {
async put(table: string, row: T): Promise<T> {
return new Promise<T>((resolve, reject) => { /* 저장 로직 */ });
}
async get(table: string, key: any): Promise<T> {
return new Promise<T>((resolve, reject) => { /* 조회 로직 */ });
}
async getTable(table: string): Promise<T[]> {
return new Promise<T[]>((resolve, reject) => { /* 테이블 전체 조회 */ });
}
}
❌ 제네릭 오남용 예시
다만 불필요한 제네릭 사용은 코드 가독성을 해칩니다:
type GType<T> = T;
type RequirementType = "USE" | "UN_USE" | "NON_SELECT";
interface Order {
getRequirement(): GType<RequirementType>; // ❌ 의미 없는 추상화
}
// 아래가 훨씬 낫다
interface Order {
getRequirement(): RequirementType;
}
🧠 마무리
제네릭은 타입스크립트에서 타입 안정성과 코드 재사용성을 동시에 잡을 수 있는 강력한 기능입니다. 특히 API 응답 처리, 유틸 함수, DB 핸들러 등에서 큰 효과를 발휘합니다. 하지만 무분별한 제네릭 사용은 오히려 혼란을 유발할 수 있으므로, 필요한 곳에 명확하게 적용하는 것이 중요할 것 같습니다ㅎ.,ㅎ
'4주차' 카테고리의 다른 글
복잡한 경로 관리는 그만! React 프로젝트에서 라우터 구조와 절대경로 설정 정리하기 (0) | 2025.05.12 |
---|---|
API 모듈화 - axios Instance 활용하기 (0) | 2025.05.12 |
아이스크림 어떤 맛 좋아하세요? 엄마는 외계인? 사빠딸? 녹차? 민트초코😍? 딸기? 망고? 블루베리? 레몬? 복숭아? 라즈베리? 피스타치오? 호두? 아몬드 봉봉? 쿠키 앤 크림? 그 중에서 하나를 고르자면... (0) | 2025.05.12 |
번들링, Tree Shaking과 Enum (0) | 2025.05.09 |
에러 처리의 필요성과 종류 (1) | 2025.05.09 |