반응형
들어가며
TypeScript 제네릭을 공부할 때 "이론은 알겠는데 실제로 어떻게 동작하는지 모르겠다"는 고민을 해본 적이 있나요? 이 글에서는 실제 코드 실행 결과와 함께 제네릭의 모든 것을 단계별로 알아보겠습니다.
마치 요리 레시피를 보면서 직접 요리를 만들어보는 것처럼, 코드를 작성하고 실행 결과를 확인하면서 제네릭을 완전히 이해해보세요!
1단계: 제네릭의 필요성 - any vs 제네릭 비교
🔍 문제 상황 파악
먼저 제네릭이 왜 필요한지 직접 확인해보겠습니다.
// 제네릭 없이 (문제가 있는 코드)
function getFirstElementAny(array: any[]): any {
return array[0];
}
// 제네릭 사용 (개선된 코드)
function getFirstElement<T>(array: T[]): T {
return array[0];
}
const numbers = [1, 2, 3, 4, 5];
const strings = ["hello", "world", "typescript"];
const booleans = [true, false, true];
// any 사용시 (타입 정보 손실)
const firstAny = getFirstElementAny(numbers);
console.log("❌ any 사용:", firstAny, "- 타입:", typeof firstAny);
// 제네릭 사용시 (타입 정보 유지)
const firstNumber = getFirstElement(numbers); // 타입 추론: number
const firstString = getFirstElement(strings); // 타입 추론: string
const firstBoolean = getFirstElement(booleans); // 타입 추론: boolean
console.log("✅ 제네릭 사용 - 숫자:", firstNumber, "- 타입:", typeof firstNumber);
console.log("✅ 제네릭 사용 - 문자:", firstString, "- 타입:", typeof firstString);
console.log("✅ 제네릭 사용 - 불린:", firstBoolean, "- 타입:", typeof firstBoolean);
📊 실행 결과
📝 1단계: 제네릭의 필요성 이해하기
----------------------------------------
숫자 배열: [ 1, 2, 3, 4, 5 ]
문자열 배열: [ 'hello', 'world', 'typescript' ]
불린 배열: [ true, false, true ]
❌ any 사용: 1 - 타입: number
→ 타입 정보가 손실됨, IDE에서 자동완성 안됨
✅ 제네릭 사용 - 숫자: 1 - 타입: number
✅ 제네릭 사용 - 문자: hello - 타입: string
✅ 제네릭 사용 - 불린: true - 타입: boolean
→ 타입 정보가 유지됨, IDE에서 자동완성 가능!
💡 핵심 포인트
- any 사용: 런타임에는 동일하게 동작하지만, TypeScript의 타입 검증 이점을 잃음
- 제네릭 사용: 타입 안전성을 유지하면서 재사용 가능한 코드 작성
2단계: 타입 지정 방법 - 명시적 vs 추론
🔧 두 가지 사용 방법
제네릭을 사용하는 방법은 크게 두 가지입니다.
// 명시적으로 타입 지정
const explicitNumber = getFirstElement<number>([10, 20, 30]);
const explicitString = getFirstElement<string>(["apple", "banana"]);
// 타입 추론 (TypeScript가 자동으로 추론)
const inferredNumber = getFirstElement([100, 200, 300]);
const inferredString = getFirstElement(["cat", "dog"]);
📊 실행 결과
📝 2단계: 타입 지정 방법들
----------------------------------------
🔧 명시적 타입 지정:
getFirstElement<number>([10, 20, 30]) = 10
getFirstElement<string>(["apple", "banana"]) = apple
🤖 타입 추론:
getFirstElement([100, 200, 300]) = 100
getFirstElement(["cat", "dog"]) = cat
→ 대부분의 경우 타입 추론이 더 편리!
💡 실무 팁
타입 추론을 우선 사용하세요! TypeScript는 대부분의 경우 정확한 타입을 추론할 수 있습니다. 명시적 타입 지정은 다음 경우에만 사용:
- 추론이 모호할 때
- 특정 타입을 강제하고 싶을 때
- 빈 배열로 작업할 때
3단계: 복합 타입과 제네릭 - 실무 데이터 처리
🏗️ 실제 프로젝트 데이터 구조
interface User {
id: number;
name: string;
email: string;
}
interface Product {
id: number;
title: string;
price: number;
}
// 객체 배열 테스트
const users: User[] = [
{ id: 1, name: "김개발", email: "kim@dev.com" },
{ id: 2, name: "이코딩", email: "lee@code.com" }
];
const products: Product[] = [
{ id: 1, title: "노트북", price: 1500000 },
{ id: 2, title: "마우스", price: 50000 }
];
const firstUser = getFirstElement(users);
const firstProduct = getFirstElement(products);
📊 실행 결과
📝 3단계: 복합 타입과 제네릭
----------------------------------------
👤 첫 번째 사용자: { id: 1, name: '김개발', email: 'kim@dev.com' }
🛍️ 첫 번째 상품: { id: 1, title: '노트북', price: 1500000 }
→ 복잡한 객체 타입도 완벽하게 처리!
놀랍죠? 하나의 함수로 숫자, 문자열뿐만 아니라 복잡한 객체 배열도 처리할 수 있습니다!
4단계: 여러 타입 매개변수 - 서로 다른 타입 조합
🔗 두 개 이상의 타입 매개변수 사용
function createPair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
// 다양한 조합 테스트
const numberStringPair = createPair(42, "hello");
const stringBooleanPair = createPair("success", true);
const userProductPair = createPair(users[0], products[0]);
📊 실행 결과
📝 4단계: 여러 타입 매개변수
----------------------------------------
🔗 숫자-문자 쌍: [ 42, 'hello' ]
🔗 문자-불린 쌍: [ 'success', true ]
🔗 사용자-상품 쌍: [
{ id: 1, name: '김개발', email: 'kim@dev.com' },
{ id: 1, title: '노트북', price: 1500000 }
]
→ 두 개의 서로 다른 타입을 조합!
🎯 활용 예시
실무에서는 이런 패턴을 자주 사용합니다:
- API 요청과 응답을 쌍으로 관리
- 키-값 쌍 데이터 처리
- 두 개의 서로 다른 데이터를 연결
5단계: 제네릭 제약 조건 - 안전한 타입 제한
🛡️ extends 키워드로 타입 제한
// id 속성을 가진 객체만 받을 수 있는 함수
interface HasId {
id: number;
}
function getId<T extends HasId>(item: T): number {
return item.id;
}
// 사용 예제
const userId = getId(users[0]); // ✅ User는 id를 가지고 있음
const productId = getId(products[0]); // ✅ Product도 id를 가지고 있음
// 에러가 나는 경우 (id가 없는 객체)
const invalidObject = { name: "test" };
// const invalidId = getId(invalidObject); // ❌ 컴파일 에러!
📊 실행 결과
📝 5단계: 제네릭 제약 조건
----------------------------------------
🆔 사용자 ID: 1
🆔 상품 ID: 1
→ id 속성이 있는 객체만 받을 수 있음!
💡 실무 활용
이런 패턴은 실무에서 매우 유용합니다:
- 데이터베이스 엔티티 처리 (모든 엔티티는 id를 가져야 함)
- API 응답 데이터 검증
- 공통 속성을 가진 객체들의 일괄 처리
6단계: keyof 연산자와 제네릭 - 동적 속성 접근
🔑 객체 속성 안전하게 접근하기
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "박개발", email: "park@dev.com", age: 25 };
// 다양한 속성 접근
const userName = getProperty(user, "name"); // string
const userAge = getProperty(user, "age"); // number
const userId2 = getProperty(user, "id"); // number
// 잘못된 키 사용시 컴파일 에러
// const invalid = getProperty(user, "salary"); // ❌ 컴파일 에러!
📊 실행 결과
📝 6단계: keyof 연산자 활용
----------------------------------------
📋 사용자 정보:
이름: 박개발 (타입: string)
나이: 25 (타입: number)
ID: 1 (타입: number)
→ 존재하는 속성만 접근 가능!
🎯 실무 응용
- 폼 데이터 처리: 동적으로 폼 필드 값 가져오기
- API 응답 파싱: 안전한 데이터 추출
- 설정 객체 관리: 타입 안전한 설정값 접근
7단계: 실용적인 제네릭 함수들 - 유틸리티 함수 작성
🧰 재사용 가능한 유틸리티 함수들
// 배열 필터링 함수
function filterBy<T>(array: T[], predicate: (item: T) => boolean): T[] {
return array.filter(predicate);
}
// 배열 변환 함수
function mapTo<T, U>(array: T[], transform: (item: T) => U): U[] {
return array.map(transform);
}
// 사용 예제
const ages = [15, 20, 25, 30, 35];
const adults = filterBy(ages, age => age >= 18);
const ageStrings = mapTo(ages, age => `${age}세`);
const activeUsers = filterBy(users, user => user.id > 0);
const userNames = mapTo(users, user => user.name);
📊 실행 결과
📝 7단계: 실용적인 제네릭 함수들
----------------------------------------
🔢 원본 나이 배열: [ 15, 20, 25, 30, 35 ]
👨💼 성인 나이 (18세 이상): [ 20, 25, 30, 35 ]
📝 나이 문자열 변환: [ '15세', '20세', '25세', '30세', '35세' ]
✅ 활성 사용자: [
{ id: 1, name: '김개발', email: 'kim@dev.com' },
{ id: 2, name: '이코딩', email: 'lee@code.com' }
]
📋 사용자 이름들: [ '김개발', '이코딩' ]
→ 타입 안전하면서 재사용 가능한 함수들!
💪 강력한 점
한 번 작성한 함수로 어떤 타입의 배열이든 처리할 수 있습니다:
- 숫자 배열 → 성인 필터링
- 문자열 변환
- 객체 배열 → 속성 추출
- 모든 과정에서 타입 안전성 보장
8단계: API 응답 처리 - 실무 핵심 패턴
🌐 실제 API 통신 패턴
interface ApiResponse<T> {
data: T;
message: string;
status: number;
timestamp: string;
}
// API 응답 처리 함수
function processApiResponse<T>(response: ApiResponse<T>): T {
console.log(`📡 API 응답 처리: ${response.message} (상태: ${response.status})`);
console.log(`⏰ 요청 시간: ${response.timestamp}`);
if (response.status === 200) {
console.log("✅ 성공적으로 데이터를 받았습니다!");
return response.data;
} else {
console.log("❌ 오류가 발생했습니다!");
throw new Error(response.message);
}
}
// 모의 API 응답 생성
const userApiResponse: ApiResponse<User> = {
data: { id: 1, name: "API사용자", email: "api@user.com" },
message: "사용자 조회 성공",
status: 200,
timestamp: new Date().toISOString()
};
const productListResponse: ApiResponse<Product[]> = {
data: [
{ id: 1, title: "키보드", price: 100000 },
{ id: 2, title: "모니터", price: 300000 }
],
message: "상품 목록 조회 성공",
status: 200,
timestamp: new Date().toISOString()
};
// API 응답 처리
const userData = processApiResponse(userApiResponse);
const productData = processApiResponse(productListResponse);
📊 실행 결과
📝 8단계: API 응답 처리 (실무 예제)
----------------------------------------
🔄 사용자 API 호출:
📡 API 응답 처리: 사용자 조회 성공 (상태: 200)
⏰ 요청 시간: 2025-06-09T07:42:18.633Z
✅ 성공적으로 데이터를 받았습니다!
📦 받은 사용자 데이터: { id: 1, name: 'API사용자', email: 'api@user.com' }
🔄 상품 목록 API 호출:
📡 API 응답 처리: 상품 목록 조회 성공 (상태: 200)
⏰ 요청 시간: 2025-06-09T07:42:18.634Z
✅ 성공적으로 데이터를 받았습니다!
📦 받은 상품 데이터: [
{ id: 1, title: '키보드', price: 100000 },
{ id: 2, title: '모니터', price: 300000 }
]
→ 같은 함수로 다양한 타입의 API 응답 처리!
🔥 실무에서 이런 식으로 사용
// 실제 사용 예시
const fetchUser = async (id: number): Promise<User> => {
const response = await fetch(`/api/users/${id}`);
const apiResponse: ApiResponse<User> = await response.json();
return processApiResponse(apiResponse);
};
const fetchProducts = async (): Promise<Product[]> => {
const response = await fetch('/api/products');
const apiResponse: ApiResponse<Product[]> = await response.json();
return processApiResponse(apiResponse);
};
9단계: 조건부 타입 - 고급 패턴 맛보기
🧠 입력에 따라 다른 타입 반환
type ApiResult<T> = T extends string ? { message: T } : { data: T };
function handleResult<T>(input: T): ApiResult<T> {
if (typeof input === 'string') {
return { message: input } as ApiResult<T>;
} else {
return { data: input } as ApiResult<T>;
}
}
// 테스트
const errorResult = handleResult("오류 메시지");
const dataResult = handleResult({ id: 1, value: "데이터" });
📊 실행 결과
📝 9단계: 조건부 타입 (고급)
----------------------------------------
📄 문자열 입력 결과: { message: '오류 메시지' }
📊 객체 입력 결과: { data: { id: 1, value: '데이터' } }
→ 입력 타입에 따라 다른 구조의 결과 반환!
🎯 고급 활용 시나리오
- 에러 처리: 문자열이면 에러 메시지, 객체면 성공 데이터
- API 응답 처리: 상황에 따른 유연한 응답 구조
- 유틸리티 함수: 입력 타입에 따른 최적화된 처리
실무 적용 가이드
📋 제네릭 도입 단계별 전략
단계 | 적용 영역 | 난이도 | 효과 |
---|---|---|---|
1단계 | 간단한 유틸리티 함수 | ⭐ | 기본 타입 안전성 |
2단계 | API 응답 타입 정의 | ⭐⭐ | 데이터 처리 안전성 |
3단계 | 재사용 가능한 컴포넌트 | ⭐⭐⭐ | 코드 재사용성 향상 |
4단계 | 복잡한 비즈니스 로직 | ⭐⭐⭐⭐ | 유지보수성 향상 |
🚀 실습 프로젝트 아이디어
- ToDo 앱: 제네릭으로 다양한 타입의 할 일 관리
- 쇼핑몰: 상품, 사용자, 주문 데이터 타입 안전 처리
- 블로그: 포스트, 댓글, 태그 등 다양한 엔티티 관리
- 대시보드: 차트 데이터, 위젯 등 동적 타입 처리
💡 베스트 프랙티스
// ✅ DO: 의미있는 타입 매개변수 이름
function createRepository<TEntity, TKey>(entityType: TEntity): Repository<TEntity, TKey> { }
// ❌ DON'T: 의미없는 한 글자 이름 남발
function process<T, U, V, W, X>(a: T, b: U, c: V, d: W, e: X) { }
// ✅ DO: 제약 조건 활용
function updateEntity<T extends { id: number }>(entity: T, updates: Partial<T>): T { }
// ✅ DO: 기본 타입 제공
interface ApiResponse<T = unknown> {
data: T;
message: string;
}
실행 환경 셋업
🛠️ 직접 실습해보기
# 1. 프로젝트 클론/생성
git clone <프로젝트-주소> 또는 새 폴더 생성
# 2. 의존성 설치
npm install
# 3. 실행
npm start
# 4. 개발 모드 (파일 변경시 자동 재실행)
npm run dev
📁 필요한 파일들
generic-practice.ts
: 메인 실습 코드tsconfig.json
: TypeScript 설정package.json
: 프로젝트 설정
마무리
🎉 핵심 포인트 요약
실행 결과를 통해 확인한 TypeScript 제네릭의 핵심 포인트들:
- 타입 안전성:
any
대신 제네릭으로 컴파일 타임 오류 방지 - 재사용성: 하나의 함수로 여러 타입 처리 가능
- 타입 추론: 대부분의 경우 명시적 타입 지정 불필요
- 제약 조건:
extends
로 안전한 타입 제한 - 실무 활용: API 처리, 유틸리티 함수 등 다양한 영역에서 활용
💪 다음 학습 단계
- 기본 숙달: 간단한 유틸리티 함수부터 시작
- 실무 적용: 현재 프로젝트의 API 호출 부분에 적용
- 고급 패턴: 조건부 타입, 매핑된 타입 등 학습
- 라이브러리 활용: React, Vue 등에서 제네릭 활용법 익히기
제네릭은 처음에는 어렵게 느껴질 수 있지만, 실제 코드를 작성하고 실행 결과를 확인하면서 학습하면 생각보다 쉽게 이해할 수 있습니다.
이제 여러분도 타입 안전하고 재사용 가능한 TypeScript 코드를 작성할 준비가 되었습니다! 🚀
참고 자료
반응형
'Frontend Development' 카테고리의 다른 글
CDN(Content Delivery Network) 완전 정복: 웹 성능 최적화의 핵심 기술 (0) | 2025.06.11 |
---|---|
TypeScript any vs 제네릭 T: 실행 결과로 보는 확실한 차이점 (0) | 2025.06.09 |
TypeScript 제네릭, 이제는 정말 쉽게 이해해보자 (0) | 2025.06.09 |
Redux vs Zustand 상태 관리 비교: 쇼핑몰 & 대시보드 실무 사례 (6) | 2025.06.01 |
Streaming SSR: 서버 사이드 렌더링의 새로운 패러다임 (0) | 2025.05.30 |