C
React/타입/Lesson 22

TypeScript + React — 컴포넌트 타입, Generic 컴포넌트

35분·theory
javascript

TypeScript + React — 컴포넌트 타입, Generic 컴포넌트

💡 왜 배워야 할까요?

🎯 버그를 코드 작성 중에 잡아서 배포 후 장애를 줄입니다.
💼 IDE가 자동완성을 해줘서 개발 속도가 30% 빨라집니다.
코드를 수정할 때 안전하게 리팩토링할 수 있습니다.
🔗 2025년 모든 중견 이상 회사는 TypeScript를 필수로 요구합니다.
🏢 실무에서는
실무에서는 '이 함수가 어떤 타입을 받는지' Props 타입을 명확히 정의해야 팀원이 실수 없이 컴포넌트를 쓸 수 있습니다. Generic 컴포넌트를 만들면 버튼, 입력창, 드롭다운 같은 다양한 UI를 하나의 코드로 관리할 수 있어 유지보수가 훨씬 쉬워집니다.

개념

TypeScript와 React를 함께 사용할 때 컴포넌트의 Props 타입 정의와 재사용 가능한 Generic 컴포넌트 구현은 2025년 현재 프론트엔드 개발의 필수 스킬입니다. 런타임 에러를 컴파일 타임에 잡아내고, IDE의 자동완성과 리팩토링을 안전하게 활용할 수 있어 대규모 프로젝트에서 생산성과 유지보수성을 크게 향상시킵니다.

왜 중요한가?

실무에서 컴포넌트 Props에 잘못된 타입의 데이터를 전달하거나, API 응답 구조가 바뀌었는데 관련 컴포넌트들을 놓치고 수정하지 않아 런타임 에러가 발생하는 경우가 빈번합니다. 또한 Table, Modal, Form 같은 공통 컴포넌트를 여러 도메인에서 재사용할 때, Generic을 활용하지 않으면 각 도메인마다 중복 코드를 작성하게 되어 유지보수 비용이 기하급수적으로 증가합니다.

핵심 개념

TypeScript + React 컴포넌트 타입핑은 마치 '레고 블록에 사용 설명서를 붙이는 것'과 같습니다. 각 컴포넌트가 어떤 Props를 받고, 어떤 형태의 데이터를 다루는지 명시하여 다른 개발자(또는 미래의 나)가 안전하게 조립할 수 있도록 합니다. Generic 컴포넌트는 '범용 틀'로, 하나의 컴포넌트로 다양한 데이터 타입을 다룰 수 있게 해주는 고급 패턴입니다.

핵심 포인트

  • Props 인터페이스로 컴포넌트 계약 명시
  • Generic을 활용한 타입 안전한 재사용 컴포넌트
  • React 19의 새로운 타입 기능과 함께 사용하는 패턴
💻 나쁜 예시 - 타입 없는 컴포넌트와 any 남발
// ❌ 타입 안전성이 없는 컴포넌트
function UserCard({ user }: any) {
  return (
    <div className="card">
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      <span>{user.age}세</span>
    </div>
  );
}

// ❌ 사용할 때도 타입 체크 없음
function App() {
  const userData: any = {
    userName: "김개발", // name이 아닌 userName
    mail: "[email protected]", // email이 아닌 mail
    years: 30 // age가 아닌 years
  };
  
  return <UserCard user={userData} />; // 런타임에서야 에러 발견
}
💻 좋은 예시 - 2025 권장 타입 안전 컴포넌트
// ✅ 명확한 Props 타입 정의
interface User {
  id: number;
  name: string;
  email: string;
  age: number;
  avatar?: string; // optional property
}

interface UserCardProps {
  user: User;
  onClick?: (userId: number) => void;
  showAge?: boolean;
}

// ✅ React 19의 최신 함수 컴포넌트 패턴
function UserCard({ user, onClick, showAge = true }: UserCardProps) {
  return (
    <div 
      className="card" 
      onClick={() => onClick?.(user.id)}
      role="button"
      tabIndex={0}
    >
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      {showAge && <span>{user.age}세</span>}
      {user.avatar && <img src={user.avatar} alt={`${user.name} 프로필`} />}
    </div>
  );
}

// ✅ 타입 안전한 사용
function App() {
  const userData: User = {
    id: 1,
    name: "김개발",
    email: "[email protected]",
    age: 30
  };
  
  const handleUserClick = (userId: number) => {
    console.log(`User ${userId} clicked`);
  };
  
  return (
    <UserCard 
      user={userData}
      onClick={handleUserClick}
      showAge={false}
    />
  );
}
💻 고급 예시 - Generic 재사용 테이블 컴포넌트
// ✅ Generic을 활용한 범용 테이블 컴포넌트
interface Column<T> {
  key: keyof T;
  title: string;
  width?: string;
  render?: (value: T[keyof T], record: T, index: number) => React.ReactNode;
}

interface DataTableProps<T> {
  data: T[];
  columns: Column<T>[];
  loading?: boolean;
  onRowClick?: (record: T, index: number) => void;
  emptyText?: string;
}

// ✅ React 19 + TypeScript 5.x 최신 패턴
function DataTable<T extends Record<string, any>>({
  data,
  columns,
  loading = false,
  onRowClick,
  emptyText = "데이터가 없습니다"
}: DataTableProps<T>) {
  if (loading) {
    return <div className="loading">로딩 중...</div>;
  }

  if (data.length === 0) {
    return <div className="empty">{emptyText}</div>;
  }

  return (
    <table className="data-table">
      <thead>
        <tr>
          {columns.map((column) => (
            <th key={String(column.key)} style={{ width: column.width }}>
              {column.title}
            </th>
          ))}
        </tr>
      </thead>
      <tbody>
        {data.map((record, index) => (
          <tr 
            key={index}
            onClick={() => onRowClick?.(record, index)}
            className={onRowClick ? 'clickable' : ''}
          >
            {columns.map((column) => (
              <td key={String(column.key)}>
                {column.render 
                  ? column.render(record[column.key], record, index)
                  : String(record[column.key] ?? '')
                }
              </td>
            ))}
          </tr>
        ))}
      </tbody>
    </table>
  );
}

// ✅ 사용 예시 - 사용자 테이블
interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user';
  lastLogin: Date;
}

function UserManagement() {
  const users: User[] = [
    { id: 1, name: "김개발", email: "[email protected]", role: "admin", lastLogin: new Date() }
  ];

  const userColumns: Column<User>[] = [
    { key: 'name', title: '이름', width: '150px' },
    { key: 'email', title: '이메일', width: '200px' },
    { 
      key: 'role', 
      title: '권한', 
      width: '100px',
      render: (role: User['role']) => (
        <span className={`badge ${role}`}>
          {role === 'admin' ? '관리자' : '사용자'}
        </span>
      )
    },
    {
      key: 'lastLogin',
      title: '최근 로그인',
      render: (date: Date) => date.toLocaleDateString('ko-KR')
    }
  ];

  return (
    <DataTable<User>
      data={users}
      columns={userColumns}
      onRowClick={(user) => console.log('선택된 사용자:', user.name)}
      emptyText="등록된 사용자가 없습니다"
    />
  );
}

💡 ⚠️ 흔한 실수

  • Props 인터페이스에서 optional과 required를 구분하지 않고 모든 필드를 필수로 만들어 컴포넌트 재사용성을 해치는 경우
  • Generic 컴포넌트에서 extends 제약 조건을 제대로 설정하지 않아 런타임에 undefined 에러가 발생하는 경우
  • event handler나 callback 함수의 매개변수 타입을 any로 정의해서 호출하는 쪽에서 타입 안전성을 잃는 경우

💡 🎯 면접 준비

Q: React 컴포넌트에서 Props 타입을 정의하는 방법과 interface vs type의 차이점을 설명해주세요
Q: Generic 컴포넌트를 만들 때 어떤 상황에서 유용하고, 실제 프로젝트에서 어떻게 활용했나요?

힌트: Props 타입 정의 → interface 사용 이유(확장성) → optional vs required 구분 → Generic 활용 사례(Table, Modal, Form) → 타입 안전성과 코드 재사용성 향상 효과 → 실제 프로젝트 적용 경험

⚛️ React 패턴 — TypeScript + React — 컴포넌트 타입, Generic 컴포넌트

TypeScript + React — 컴포넌트 타입, Generic 컴포넌트을 React에서 어떻게 쓰는지 코드와 함께 단계별로 익혀보세요.
1 🧩 1. 컴포넌트 정의
함수가 곧 컴포넌트
function Greeting({ name }) {
  return <h1>안녕, {name}!</h1>;
}
2 📤 2. Props 전달
부모 → 자식으로 데이터 단방향 흐름
<Greeting name="홍길동" />
3 🔁 3. 재사용
같은 컴포넌트, 다른 props로 여러 번
<Greeting name="Alice" />
<Greeting name="Bob" />
4 💡 4. 단방향 데이터 흐름
자식이 부모 state를 바꾸려면 콜백 함수를 props로 받음

🎮 TypeScript + React — 컴포넌트 타입, Generic 컴포넌트 — 단계별 이해

각 단계를 클릭해 내용을 읽고 ✓ 이해했어요 버튼으로 진행률을 확인하세요.
🖥️ 실행 결과 — 렌더링된 React 컴포넌트
✏️ React 코드 수정하기 (클릭해서 열기)
⚛️ React 18 + Babel Standalone — 결과 먼저 확인 후 코드 편집기에서 직접 수정해볼 수 있습니다.

확인 퀴즈

React 컴포넌트 props 타입을 정의할 때 권장하는 방법은?
💡 interface Props { name: string; age: number } 또는 type Props = {...}로 정의하고 function Component({ name, age }: Props)처럼 사용해요. TypeScript가 props 타입 오류를 컴파일 타임에 잡아줘요.
TypeScript + React - React