💻 프로그래밍

Generic

제네릭

타입을 파라미터화하는 기법. 코드 재사용성과 타입 안전성 향상.

📖 상세 설명

제네릭(Generic)은 타입을 매개변수처럼 전달받아 다양한 타입에서 동작하는 코드를 작성하는 프로그래밍 기법입니다. 컴파일 시점에 타입이 결정되므로 런타임 에러 없이 타입 안전성을 보장하면서도 코드 중복을 크게 줄일 수 있습니다.

제네릭이 없던 시절에는 모든 타입을 처리하려면 Object나 any를 사용하거나, 각 타입마다 동일한 로직의 함수를 별도로 작성해야 했습니다. 제네릭을 사용하면 하나의 함수나 클래스로 숫자, 문자열, 객체 등 어떤 타입이든 안전하게 처리할 수 있어 DRY(Don't Repeat Yourself) 원칙을 지킬 수 있습니다.

제네릭은 TypeScript, Java, C#, Go, Rust 등 대부분의 현대 정적 타입 언어에서 지원합니다. 특히 컬렉션(배열, 리스트, 맵 등), API 응답 처리, 유틸리티 함수 등에서 필수적으로 사용되며, 라이브러리나 프레임워크를 개발할 때 핵심적인 역할을 합니다.

실무에서 제네릭은 API 응답 타입 정의, 상태 관리 라이브러리의 스토어 타입, React 컴포넌트의 Props 타입 등 다양한 곳에서 활용됩니다. 특히 TypeScript 프로젝트에서는 제네릭을 잘 활용하면 IDE의 자동완성과 타입 추론 기능을 극대화하여 개발 생산성을 크게 높일 수 있습니다.

💻 코드 예제

// 1. 기본 제네릭 함수 - 타입을 매개변수로 받음
function identity<T>(value: T): T {
    return value;
}

// 사용 시 타입이 자동 추론됨
const num = identity(42);        // T = number
const str = identity("hello");   // T = string

// 2. 제네릭 인터페이스 - API 응답 타입 정의
interface ApiResponse<T> {
    data: T;
    status: number;
    message: string;
    timestamp: Date;
}

interface User {
    id: number;
    name: string;
    email: string;
}

// 사용자 API 응답
const userResponse: ApiResponse<User> = {
    data: { id: 1, name: "김철수", email: "kim@example.com" },
    status: 200,
    message: "성공",
    timestamp: new Date()
};

// 3. 제네릭 제약조건 (extends) - 특정 타입만 허용
interface HasLength {
    length: number;
}

function logLength<T extends HasLength>(item: T): void {
    console.log(`길이: ${item.length}`);
}

logLength("hello");        // 문자열은 length 있음 ✅
logLength([1, 2, 3]);      // 배열도 length 있음 ✅
// logLength(123);         // 숫자는 length 없음 ❌ 컴파일 에러

// 4. 제네릭 유틸리티 함수 - 배열에서 첫 번째 요소 추출
function first<T>(arr: T[]): T | undefined {
    return arr[0];
}

const firstNum = first([1, 2, 3]);     // number | undefined
const firstUser = first([userResponse.data]); // User | undefined

// 5. 제네릭 클래스 - 타입 안전한 스택 구현
class Stack<T> {
    private items: T[] = [];

    push(item: T): void {
        this.items.push(item);
    }

    pop(): T | undefined {
        return this.items.pop();
    }

    peek(): T | undefined {
        return this.items[this.items.length - 1];
    }

    isEmpty(): boolean {
        return this.items.length === 0;
    }
}

const numberStack = new Stack<number>();
numberStack.push(10);
numberStack.push(20);
console.log(numberStack.pop()); // 20 (number 타입)

🗣️ 실무에서 이렇게 말하세요

💬 회의에서
"API 응답 타입을 매번 새로 만들지 말고, ApiResponse<T> 제네릭으로 공통화합시다. data 부분만 제네릭 타입으로 받으면 모든 엔드포인트에서 재사용할 수 있어요."
💬 면접에서
"제네릭은 컴파일 타임에 타입 안전성을 보장하면서 코드 재사용성을 높이는 기법입니다. any를 쓰면 런타임 에러 위험이 있지만, 제네릭은 타입 추론과 자동완성을 지원해서 개발 생산성도 높아집니다."
💬 코드 리뷰에서
"여기 Object로 받고 있는데, 제네릭으로 바꾸면 어떨까요? extends 제약조건으로 필요한 프로퍼티만 강제하면 타입 캐스팅 없이도 안전하게 사용할 수 있습니다."

⚠️ 흔한 실수 & 주의사항

❌
any 대신 제네릭 사용하기

any를 쓰면 타입 검사를 포기하게 됩니다. "일단 any로 해두자"라고 생각하면 나중에 런타임 에러로 돌아옵니다. 타입을 모르면 unknown이나 제네릭을 사용하세요.

❌
제네릭 타입 파라미터 이름 관례

T, U, V 같은 단일 문자 대신 의미 있는 이름을 쓰세요. TData, TResponse, TItem처럼 T 접두사와 함께 의미를 담으면 코드 가독성이 높아집니다.

✅
제약조건(extends)으로 타입 범위 제한

제네릭이 모든 타입을 받을 필요는 없습니다. extends를 사용해 필요한 속성이나 메서드를 가진 타입만 받도록 제한하면 더 안전한 코드를 작성할 수 있습니다.

🔗 관련 용어

📚 더 배우기