← 홈페이지 5강 목록으로
🧬
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 dev

2. 컴포넌트 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>;
}
예제 코드 / 강의 자료

전체 강의 자료와 예제 코드는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗

댓글 0

아직 댓글이 없습니다. 첫 댓글을 남겨 주세요.

댓글은 회원만 쓸 수 있습니다.

광고·욕설·비방·개인정보·불법 내용이 담긴 댓글은 숨기거나 지웁니다. 댓글은 닉네임과 함께 공개됩니다. 개인정보처리방침