🧬
EPISODE 05
<T> · 제약 · keyof · Partial/Pick/Omit/Record · Props 타입 · useState<T> · 이벤트 타입
제네릭 · React + TypeScript
타입을 매개변수처럼 사용하는 제네릭으로 재사용 가능한 함수·인터페이스·클래스를 작성하고, keyof와 유틸리티 타입(Partial/Pick/Omit/Record)을 활용합니다. React 컴포넌트에 TypeScript 적용하기 — Props 인터페이스, useState<T>, 이벤트 타입(ChangeEvent/FormEvent/MouseEvent), API 응답 타입 지정까지.
genericskeyofutility typesReactTypeScriptuseStateevents
소요 시간
⏱ 45~60분 + 45분
난이도
📊 중급
선수 조건
🎯 ts-04
결과물
ApiResponse<T>, Stack<T> 같은 재사용 가능한 타입 작성 / 타입이 적용된 컴포넌트와 폼 핸들러
이 강의에서 배우는 것
- 1제네릭 함수/인터페이스/클래스를 정의한다
- 2T extends ... 로 제네릭 제약을 건다
- 3keyof로 객체의 키 타입을 추출한다
- 4Partial/Pick/Omit/Record/ReturnType 같은 유틸리티를 활용한다
- 5create-next-app 또는 Vite로 React+TS 프로젝트를 만든다
- 6interface Props 로 컴포넌트 props 타입을 정의한다
- 7useState<T> 로 상태 타입을 명시한다
- 8ChangeEvent / FormEvent / MouseEvent 의 제네릭 인자를 안다
- 9fetch 응답을 타입으로 보호한다
제네릭
타입을 매개변수처럼 사용하는 제네릭으로 재사용 가능한 함수·인터페이스·클래스를 작성하고, keyof와 유틸리티 타입(Partial/Pick/Omit/Record)을 활용합니다.
1. 제네릭이란
typescript
// 제네릭 없이
function getFirstNumber(arr: number[]): number { return arr[0]; }
function getFirstString(arr: string[]): string { return arr[0]; }
// 제네릭
function getFirst<T>(arr: T[]): T { return arr[0]; }
getFirst<number>([1, 2, 3]); // T = number
getFirst<string>(['a', 'b']); // T = string
getFirst([true, false]); // T = boolean (추론)2. 제네릭 인터페이스
typescript
interface ApiResponse<T> {
success: boolean;
data: T;
message?: string;
timestamp: string;
}
const userResponse: ApiResponse<User> = {
success: true,
data: { id: 1, name: '홍길동' },
timestamp: new Date().toISOString(),
};
const listResponse: ApiResponse<Product[]> = {
success: true, data: [...products], timestamp: '...',
};3. 제네릭 클래스
typescript
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]; }
get size(): number { return this.items.length; }
}
const numberStack = new Stack<number>();
const stringStack = new Stack<string>();4. 제약 (extends)
typescript
// T는 반드시 { name: string } 형태
function printName<T extends { name: string }>(item: T): void {
console.log(item.name);
}
printName({ name: '홍길동', age: 30 }); // OK
printName({ age: 30 }); // 오류: name 없음5. keyof
typescript
interface User {
id: number;
name: string;
email: string;
}
type UserKey = keyof User; // 'id' | 'name' | 'email'
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: '홍길동', email: 'hong@…' };
getProperty(user, 'name'); // string
getProperty(user, 'id'); // number
// getProperty(user, 'age'); // 오류6. 유틸리티 타입
typescript
interface User {
id: number; name: string; email: string; password: string;
}
// Partial — 모든 프로퍼티 선택적
type PartialUser = Partial<User>;
// Required — 모든 프로퍼티 필수
type RequiredUser = Required<PartialUser>;
// Pick — 특정 프로퍼티만
type PublicUser = Pick<User, 'id' | 'name' | 'email'>;
// Omit — 특정 프로퍼티 제외
type SafeUser = Omit<User, 'password'>;
// Record — 키-값 매핑
type RolePermissions = Record<'admin' | 'user' | 'guest', string[]>;
// ReturnType — 함수 반환 타입 추출
function createUser() { return { id: 1, name: '홍길동' }; }
type CreatedUser = ReturnType<typeof createUser>;React + TypeScript
React 컴포넌트에 TypeScript 적용하기 — Props 인터페이스, useState<T>, 이벤트 타입(ChangeEvent/FormEvent/MouseEvent), API 응답 타입 지정까지.
1. 프로젝트 생성
bash
# Next.js
npx create-next-app@latest my-app --typescript
# React (Vite)
npm create vite@latest my-app -- --template react-ts
cd my-app && npm install && npm run dev2. 컴포넌트 Props 타입
tsx
interface ButtonProps {
label: string;
onClick: () => void;
color?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
}
// 일반 함수 (권장)
function Button({ label, onClick, color = 'primary', disabled }: ButtonProps) {
return (
<button onClick={onClick} disabled={disabled} className={color}>
{label}
</button>
);
}
// React.FC 방식
const Button: React.FC<ButtonProps> = ({ label, onClick }) => {
return <button onClick={onClick}>{label}</button>;
};3. useState<T>
tsx
import { useState } from 'react';
const [count, setCount] = useState<number>(0);
const [name, setName] = useState<string>('');
const [isVisible, setIsVisible] = useState<boolean>(false);
interface User { name: string; email: string; }
const [user, setUser] = useState<User | null>(null);
interface Todo { id: number; text: string; done: boolean; }
const [todos, setTodos] = useState<Todo[]>([]);4. 이벤트 타입
tsx
// input 변경
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
// textarea / select 도 동일 패턴
const handleTextArea = (e: React.ChangeEvent<HTMLTextAreaElement>) => {};
const handleSelect = (e: React.ChangeEvent<HTMLSelectElement>) => {};
// 폼 제출
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
};
// 버튼 클릭
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {};5. API 응답 타입
tsx
interface Post {
id: number; title: string; body: string; userId: number;
}
function PostList() {
const [posts, setPosts] = useState<Post[]>([]);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/posts')
.then((res) => res.json())
.then((data: Post[]) => {
setPosts(data);
setLoading(false);
});
}, []);
if (loading) return <p>로딩 중...</p>;
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 주세요.
댓글은 회원만 쓸 수 있습니다.
광고·욕설·비방·개인정보·불법 내용이 담긴 댓글은 숨기거나 지웁니다. 댓글은 닉네임과 함께 공개됩니다. 개인정보처리방침