안녕하세요! 타입스크립트 시리즈 다섯 번째 시간입니다. 오늘은 타입스크립트의 가장 강력한 기능 중 하나인 **제네릭(Generics)**에 대해 알아보겠습니다. 제네릭을 이해하면 타입 안전성을 유지하면서도 재사용 가능한 컴포넌트를 작성할 수 있습니다.
제네릭이란?
제네릭은 다양한 타입에서 작동할 수 있는 컴포넌트를 만들 수 있게 해주는 기능입니다. 특정 타입에 종속되지 않으면서도 타입 안전성을 보장합니다. 쉽게 말해, 함수나 클래스가 다양한 타입에 대해 동작하도록 만들 수 있습니다.
기본 제네릭 문법
제네릭 함수의 기본 구문은 다음과 같습니다:
function identity<T>(arg: T): T {
return arg;
}
// 사용 방법 1: 타입 명시
const result1 = identity<string>("Hello"); // 반환 타입은 string
// 사용 방법 2: 타입 추론 (일반적으로 더 간결하고 권장됨)
const result2 = identity("World"); // 타입스크립트가 string으로 추론
위 예제에서 T는 타입 변수로, 사용자가 제공한 타입을 캡처하고 이후에 해당 타입을 참조하는 데 사용합니다.
제네릭 인터페이스
제네릭은 인터페이스에도 적용할 수 있습니다:
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
const myIdentity: GenericIdentityFn<number> = identity;
const result = myIdentity(42); // 반환 타입은 number
제네릭 클래스
클래스에도 제네릭을 적용할 수 있습니다:
class Box<T> {
private content: T;
constructor(value: T) {
this.content = value;
}
getValue(): T {
return this.content;
}
setValue(value: T): void {
this.content = value;
}
}
// 문자열 상자
const stringBox = new Box<string>("Hello TypeScript");
console.log(stringBox.getValue()); // "Hello TypeScript"
stringBox.setValue("Hello Generics");
console.log(stringBox.getValue()); // "Hello Generics"
// 숫자 상자
const numberBox = new Box(100); // 타입 추론으로 Box<number>
console.log(numberBox.getValue()); // 100
// numberBox.setValue("string"); // 오류: string 타입은 number 타입에 할당할 수 없음
제네릭 제약조건(Generic Constraints)
때로는 제네릭 타입에 특정 기능이나 프로퍼티가 있어야 할 때가 있습니다. 이럴 때 제약조건을 사용할 수 있습니다:
// 'length' 프로퍼티를 가진 타입만 허용
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(`길이: ${arg.length}`);
return arg;
}
logLength("Hello"); // 길이: 5
logLength([1, 2, 3, 4]); // 길이: 4
logLength({ length: 10 }); // 길이: 10
// logLength(3); // 오류: number 타입에는 'length' 프로퍼티가 없음
타입 매개변수에 제약조건 사용
한 타입 매개변수를 다른 타입 매개변수의 제약조건으로 사용할 수도 있습니다:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const person = {
name: "Alice",
age: 30,
location: "Seoul"
};
console.log(getProperty(person, "name")); // "Alice"
console.log(getProperty(person, "age")); // 30
// console.log(getProperty(person, "salary")); // 오류: 'salary'는 'name' | 'age' | 'location' 타입에 없음
제네릭 기본값(Default Type Parameters)
타입 매개변수에 기본값을 지정할 수 있습니다:
interface ApiResponse<T = any> {
data: T;
status: number;
message: string;
}
// 기본 any 타입 사용
const response1: ApiResponse = {
data: "Some data",
status: 200,
message: "Success"
};
// 구체적인 타입 지정
interface User {
id: number;
name: string;
}
const response2: ApiResponse<User> = {
data: { id: 1, name: "Alice" },
status: 200,
message: "User fetched successfully"
};
제네릭 유틸리티 타입
타입스크립트는 많은 내장 제네릭 유틸리티 타입을 제공합니다. 몇 가지 유용한 예를 살펴보겠습니다:
Partial<T>
모든 프로퍼티를 선택적으로 만듭니다:
interface Todo {
title: string;
description: string;
completed: boolean;
}
function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>): Todo {
return { ...todo, ...fieldsToUpdate };
}
const todo = {
title: "Learn TypeScript",
description: "Study generics",
completed: false
};
const updatedTodo = updateTodo(todo, {
description: "Study advanced generics", // 일부 필드만 업데이트
completed: true
});
console.log(updatedTodo);
// {
// title: "Learn TypeScript",
// description: "Study advanced generics",
// completed: true
// }
Readonly<T>
모든 프로퍼티를 읽기 전용으로 만듭니다:
const readonlyTodo: Readonly<Todo> = {
title: "Learn TypeScript",
description: "Study generics",
completed: false
};
// readonlyTodo.completed = true; // 오류: readonly 프로퍼티에 할당할 수 없음
Pick<T, K>
특정 프로퍼티만 선택합니다:
type TodoPreview = Pick<Todo, "title" | "completed">;
const todoPreview: TodoPreview = {
title: "Learn TypeScript",
completed: false
// description: "Study generics" // 오류: 이 프로퍼티는 타입에 없음
};
Omit<T, K>
특정 프로퍼티를 제외합니다:
type TodoSummary = Omit<Todo, "description">;
const todoSummary: TodoSummary = {
title: "Learn TypeScript",
completed: false
// description 프로퍼티는 제외됨
};
Record<K, T>
프로퍼티 K 타입의 키와 T 타입의 값을 가진 객체 타입을 생성합니다:
type PageInfo = {
title: string;
url: string;
};
type Pages = Record<string, PageInfo>;
const sitePages: Pages = {
home: { title: "홈", url: "/" },
about: { title: "소개", url: "/about" },
contact: { title: "연락처", url: "/contact" }
};
조건부 타입(Conditional Types)
조건에 따라 타입을 결정할 수 있는 강력한 기능입니다:
type NonNullable<T> = T extends null | undefined ? never : T;
type StringOrNumber = string | number | null | undefined;
// null과 undefined를 제외한 타입
type NonNullStringOrNumber = NonNullable<StringOrNumber>; // string | number
실전 예제: 제네릭 API 클라이언트
제네릭을 활용한 실용적인 예제를 살펴보겠습니다:
interface ApiResponse {
data: T;
status: number;
message: string;
}
class ApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async get(endpoint: string): Promise<apiresponse> {
const response = await fetch(`${this.baseUrl}${endpoint}`);
const json = await response.json();
return json as ApiResponse;
}
async post<t, u="">(endpoint: string, data: T): Promise<apiresponse> {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const json = await response.json();
return json as ApiResponse;
}
}
interface User {
id: number;
name: string;
email: string;
}
interface CreateUserRequest {
name: string;
email: string;
password: string;
}
const apiClient = new ApiClient('https://api.example.com');
// 사용 예시
async function example() {
// GET 요청 - User 타입 지정
const userResponse = await apiClient.get('/users/1');
console.log(userResponse.data.name); // 타입 안전성 보장
// POST 요청 - CreateUserRequest로 요청하고 User로 응답 받음
const createUserResponse = await apiClient.post<createuserrequest, user="">(
'/users',
{
name: "New User",
email: "user@example.com",
password: "password123"
}
);
console.log(createUserResponse.data.id); // 새로 생성된 사용자의 ID
}
</createuserrequest,></apiresponse</t,></apiresponse
제네릭 활용 팁
- 의미 있는 타입 변수 이름 사용하기: T, U, V 같은 단일 문자 대신 TData, TResponse 처럼 의미 있는 이름을 사용하면 코드가 더 이해하기 쉬워집니다.
- 가능하면 타입 추론 활용하기: 명시적으로 타입을 선언하는 것보다 타입스크립트의 추론 기능을 활용하면 코드가 더 간결해집니다.
- 제약조건 활용하기: 제네릭 타입에 제약조건을 설정하면 더 명확한 오류 메시지를 얻을 수 있습니다.
- 내장 제네릭 유틸리티 타입 활용하기: 타입스크립트의 내장 유틸리티 타입을 활용하면 코드를 더 간결하게 작성할 수 있습니다.
마치며
제네릭은 타입스크립트의 가장 강력한 기능 중 하나로, 타입 안전성을 유지하면서도 재사용 가능한 컴포넌트를 작성할 수 있게 해줍니다. 처음에는 복잡해 보일 수 있지만, 익숙해지면 코드의 유연성과 안정성을 크게 향상시킬 수 있습니다.
다음 포스팅에서는 타입스크립트의 고급 타입(Advanced Types)에 대해 알아보겠습니다. 타입스크립트 시리즈를 계속 지켜봐 주세요!