React + TypeScript 도입과 패턴

2025. 4. 2. 15:29Frameworks/React

React + TypeScript 도입과 패턴

TypeScript란 무엇인가요?

TypeScript는 JavaScript에 **타입(Type)**을 추가한 언어입니다. 쉽게 말해 변수, 함수, 객체가 어떤 형태인지 미리 정해놓고 사용하는 방식입니다.

왜 React에 TypeScript를 사용해야 할까요?

  • 실수 예방: 코드를 작성할 때 실수를 미리 잡아줍니다
  • 편리한 자동완성: 어떤 속성이 있는지 자동으로 알려줍니다
  • 유지보수 용이: 나중에 코드를 수정할 때도 쉽게 이해할 수 있습니다

React + TypeScript 시작하기

새 프로젝트 생성하기

# TypeScript로 React 프로젝트 시작하기
npx create-react-app my-app --template typescript

기존 프로젝트에 추가하기

# 기존 프로젝트에 TypeScript 추가하기
npm install --save typescript @types/node @types/react @types/react-dom

기본적인 타입 패턴

1. Props 타입 지정하기

// 기본 방식
type ButtonProps = {
  text: string;
  onClick: () => void;
  color?: string; // ? 는 선택적(optional) 속성
};

function Button({ text, onClick, color = 'blue' }: ButtonProps) {
  return (
    <button onClick={onClick} style={{ backgroundColor: color }}>
      {text}
    </button>
  );
}

2. 상태(State) 타입 지정하기

// useState에 타입 지정하기
import { useState } from 'react';

function Counter() {
  // number 타입의 상태 선언
  const [count, setCount] = useState<number>(0);
  
  // 객체 타입의 상태 선언
  const [user, setUser] = useState<{name: string; age: number}>({
    name: '',
    age: 0
  });
  
  return (
    <div>
      <p>현재 카운트: {count}</p>
      <button onClick={() => setCount(count + 1)}>증가</button>
    </div>
  );
}

3. 이벤트 핸들러 타입 지정하기

import { ChangeEvent, FormEvent } from 'react';

function LoginForm() {
  // 폼 제출 이벤트 타입 지정
  const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    // 폼 제출 로직
  };
  
  // 입력 변경 이벤트 타입 지정
  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    console.log(e.target.value);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input type="text" onChange={handleChange} />
      <button type="submit">로그인</button>
    </form>
  );
}

유용한 TypeScript 패턴

1. 인터페이스 확장하기

// 기본 인터페이스
interface BaseProps {
  className?: string;
  style?: React.CSSProperties;
}

// 확장된 인터페이스
interface ButtonProps extends BaseProps {
  text: string;
  onClick: () => void;
}

function Button({ text, onClick, className, style }: ButtonProps) {
  return (
    <button onClick={onClick} className={className} style={style}>
      {text}
    </button>
  );
}

2. 유니온 타입 활용하기

// 버튼 크기를 지정하는 유니온 타입
type ButtonSize = 'small' | 'medium' | 'large';
type ButtonVariant = 'primary' | 'secondary' | 'danger';

type ButtonProps = {
  size: ButtonSize;
  variant: ButtonVariant;
  label: string;
};

function Button({ size, variant, label }: ButtonProps) {
  let padding;
  let backgroundColor;
  
  // size에 따라 다른 스타일 적용
  if (size === 'small') padding = '4px 8px';
  else if (size === 'medium') padding = '8px 16px';
  else padding = '12px 24px';
  
  // variant에 따라 배경색 지정
  if (variant === 'primary') backgroundColor = 'blue';
  else if (variant === 'secondary') backgroundColor = 'gray';
  else backgroundColor = 'red';
  
  return (
    <button style={{ padding, backgroundColor }}>
      {label}
    </button>
  );
}

3. Children 타입 지정하기

import { ReactNode } from 'react';

type CardProps = {
  title: string;
  children: ReactNode; // JSX 요소나 문자열 등 모든 자식 요소 허용
};

function Card({ title, children }: CardProps) {
  return (
    <div className="card">
      <h2>{title}</h2>
      <div className="card-content">{children}</div>
    </div>
  );
}

// 사용 예시
function App() {
  return (
    <Card title="환영합니다">
      <p>카드 내용입니다.</p>
      <button>자세히 보기</button>
    </Card>
  );
}

고급 TypeScript 패턴

1. 제네릭 컴포넌트 만들기

// 제네릭을 사용한 재사용 가능한 목록 컴포넌트
type ListProps<T> = {
  items: T[];
  renderItem: (item: T) => ReactNode;
};

function List<T>({ items, renderItem }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

// 사용 예시
function App() {
  const users = [
    { id: 1, name: '김철수' },
    { id: 2, name: '이영희' }
  ];
  
  return (
    <List 
      items={users} 
      renderItem={(user) => <span>{user.name}</span>}
    />
  );
}

2. 타입 가드 활용하기

// 서로 다른 타입의 항목을 처리하는 경우
type TextItem = {
  type: 'text';
  content: string;
};

type ImageItem = {
  type: 'image';
  url: string;
  alt: string;
};

type ContentItem = TextItem | ImageItem;

function ContentRenderer({ item }: { item: ContentItem }) {
  // 타입 가드를 사용하여 올바른 렌더링 로직 적용
  if (item.type === 'text') {
    return <p>{item.content}</p>;
  } else {
    return <img src={item.url} alt={item.alt} />;
  }
}

실전 팁과 트릭

1. API 응답 타입 정의하기

// API 응답 타입 정의
interface User {
  id: number;
  name: string;
  email: string;
}

// 데이터를 가져오는 훅
function useUsers() {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then((data: User[]) => {
        setUsers(data);
        setLoading(false);
      });
  }, []);
  
  return { users, loading };
}

2. 타입 정의 파일 분리하기

// types.ts 파일에 타입 정의 모음
export interface User {
  id: number;
  name: string;
  email: string;
}

export interface Post {
  id: number;
  title: string;
  content: string;
  userId: number;
}

export type CommentType = {
  id: number;
  postId: number;
  text: string;
  author: string;
};

3. 커스텀 훅에 타입 적용하기

import { useState, useEffect } from 'react';

// 제네릭을 활용한 재사용 가능한 데이터 가져오기 훅
function useFetch<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);
  
  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch(url);
        if (!response.ok) {
          throw new Error('네트워크 응답이 올바르지 않습니다');
        }
        const result = await response.json();
        setData(result);
      } catch (error) {
        setError(error instanceof Error ? error : new Error('알 수 없는 오류'));
      } finally {
        setLoading(false);
      }
    };
    
    fetchData();
  }, [url]);
  
  return { data, loading, error };
}

// 사용 예시
interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

function TodoList() {
  const { data, loading, error } = useFetch<Todo[]>('https://jsonplaceholder.typicode.com/todos');
  
  if (loading) return <div>로딩 중...</div>;
  if (error) return <div>오류: {error.message}</div>;
  if (!data) return <div>데이터가 없습니다</div>;
  
  return (
    <ul>
      {data.map(todo => (
        <li key={todo.id}>
          {todo.completed ? '✅' : '❌'} {todo.title}
        </li>
      ))}
    </ul>
  );
}

 

요약

  • TypeScript는 React 개발에 안정성과 생산성을 더해줍니다
  • 기본적인 패턴으로 Props, State, 이벤트 타입 지정하기
  • 고급 패턴으로 인터페이스 확장, 유니온 타입, 제네릭 활용하기
  • 실제 개발에서는 API 응답 타입 정의, 타입 파일 분리 등의 방법 사용하기

'Frameworks > React' 카테고리의 다른 글

React 성능 분석 실전  (0) 2025.04.02
테스트 전략 (Jest, RTL, Cypress)  (0) 2025.04.02
React 애플리케이션 성능 최적화  (0) 2025.04.01
React 폼 관리와 유효성 검사  (0) 2025.04.01
React 컴포넌트 스타일링 방법  (0) 2025.04.01