TypeScript 제네릭 완전 정복: 실행 결과로 배우는 실전 가이드

2025. 6. 9. 16:52·Frontend Development
반응형

들어가며

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단계 복잡한 비즈니스 로직 ⭐⭐⭐⭐ 유지보수성 향상

🚀 실습 프로젝트 아이디어

  1. ToDo 앱: 제네릭으로 다양한 타입의 할 일 관리
  2. 쇼핑몰: 상품, 사용자, 주문 데이터 타입 안전 처리
  3. 블로그: 포스트, 댓글, 태그 등 다양한 엔티티 관리
  4. 대시보드: 차트 데이터, 위젯 등 동적 타입 처리

💡 베스트 프랙티스

// ✅ 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 제네릭의 핵심 포인트들:

  1. 타입 안전성: any 대신 제네릭으로 컴파일 타임 오류 방지
  2. 재사용성: 하나의 함수로 여러 타입 처리 가능
  3. 타입 추론: 대부분의 경우 명시적 타입 지정 불필요
  4. 제약 조건: extends로 안전한 타입 제한
  5. 실무 활용: API 처리, 유틸리티 함수 등 다양한 영역에서 활용

💪 다음 학습 단계

  1. 기본 숙달: 간단한 유틸리티 함수부터 시작
  2. 실무 적용: 현재 프로젝트의 API 호출 부분에 적용
  3. 고급 패턴: 조건부 타입, 매핑된 타입 등 학습
  4. 라이브러리 활용: React, Vue 등에서 제네릭 활용법 익히기

제네릭은 처음에는 어렵게 느껴질 수 있지만, 실제 코드를 작성하고 실행 결과를 확인하면서 학습하면 생각보다 쉽게 이해할 수 있습니다.

이제 여러분도 타입 안전하고 재사용 가능한 TypeScript 코드를 작성할 준비가 되었습니다! 🚀


참고 자료

  • TypeScript 공식 문서 - Generics
  • TypeScript Deep Dive - Generics
  • Microsoft TypeScript Handbook
  • TypeScript Playground - 온라인에서 바로 실습
반응형
저작자표시 비영리 변경금지 (새창열림)

'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
'Frontend Development' 카테고리의 다른 글
  • CDN(Content Delivery Network) 완전 정복: 웹 성능 최적화의 핵심 기술
  • TypeScript any vs 제네릭 T: 실행 결과로 보는 확실한 차이점
  • TypeScript 제네릭, 이제는 정말 쉽게 이해해보자
  • Redux vs Zustand 상태 관리 비교: 쇼핑몰 & 대시보드 실무 사례
Kun Woo Kim
Kun Woo Kim
안녕하세요, 김건우입니다! 웹과 앱 개발에 열정적인 전문가로, React, TypeScript, Next.js, Node.js, Express, Flutter 등을 활용한 프로젝트를 다룹니다. 제 블로그에서는 개발 여정, 기술 분석, 실용적 코딩 팁을 공유합니다. 창의적인 솔루션을 실제로 적용하는 과정의 통찰도 나눌 예정이니, 궁금한 점이나 상담은 언제든 환영합니다.
  • Kun Woo Kim
    WhiteMouseDev
    김건우
  • 깃허브
    포트폴리오
    velog
  • 전체
    오늘
    어제
  • 공지사항

    • [인사말] 이제 티스토리에서도 만나요! WhiteMouse⋯
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • 분류 전체보기 (100) N
      • Frontend Development (39) N
      • Backend Development (21) N
      • Algorithm (33) N
        • 백준 (11) N
        • 프로그래머스 (17)
        • 알고리즘 (5)
      • Infra (1)
      • 자료구조 (3)
  • 링크

    • Github
    • Portfolio
    • Velog
  • 인기 글

  • 태그

    frontend development
    tailwindcss
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Kun Woo Kim
TypeScript 제네릭 완전 정복: 실행 결과로 배우는 실전 가이드
상단으로

티스토리툴바