Updated:

기본 타입 정의

// 기본 타입 예제
let isDone: boolean = false;
let decimal: number = 6;
let color: string = "blue";
let list: number[] = [1, 2, 3];
let tuple: [string, number] = ["hello", 10];
let anyValue: any = 4;

// 열거형
enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT"
}

// 함수 타입
function add(x: number, y: number): number {
  return x + y;
}

// 선택적 매개변수와 기본값
function buildName(firstName: string, lastName?: string): string {
  return lastName ? `${firstName} ${lastName}` : firstName;
}

// 함수 오버로드
function getLength(value: string): number;
function getLength(value: any[]): number;
function getLength(value: string | any[]): number {
  return value.length;
}

// 인터페이스
interface User {
  id: number;
  name: string;
  email: string;
  age?: number; // 선택적 속성
  readonly createdAt: Date; // 읽기 전용 속성
}

// 타입 별칭
type ID = string | number;
type UserWithID = User & { id: ID };

// 유니온 타입과 교차 타입
type Result = "success" | "failure";
type UserOrAdmin = User | { role: string; permissions: string[] };
type EnhancedUser = User & { role: string };

// 제네릭
function identity<T>(arg: T): T {
  return arg;
}

// 제네릭 인터페이스
interface Box<T> {
  value: T;
}

// 제네릭 클래스
class Queue<T> {
  private data: T[] = [];

  push(item: T): void {
    this.data.push(item);
  }

  pop(): T | undefined {
    return this.data.shift();
  }
}

// 유틸리티 타입 예제
interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

// Partial: 모든 속성을 선택적으로 만듦
type PartialTodo = Partial<Todo>;

// Required: 모든 속성을 필수로 만듦
type RequiredTodo = Required<Todo>;

// Pick: 특정 속성만 선택
type TodoPreview = Pick<Todo, "title" | "completed">;

// Omit: 특정 속성을 제외
type TodoWithoutDescription = Omit<Todo, "description">;

// Record: 키-값 쌍의 타입 정의
type TodoRecord = Record<string, Todo>;

코멘트: TypeScript는 JavaScript에 정적 타입 시스템을 추가한 언어로, 코드 품질과 개발자 경험을 향상시킵니다. 주요 타입 기능은 다음과 같습니다:

  1. 기본 타입: boolean, number, string, array, tuple, any, void 등
  2. 인터페이스: 객체 구조를 정의하는 강력한 방법
  3. 타입 별칭: 기존 타입에 새 이름을 부여하거나 복잡한 타입 정의
  4. 유니온과 교차 타입: 타입 조합을 위한 OR( )와 AND(&) 연산
  5. 제네릭: 재사용 가능한 컴포넌트를 다양한 타입에 대해 작업할 수 있게 함
  6. 유틸리티 타입: 기존 타입을 변환하는 내장 도구 (Partial, Required, Pick 등)

TypeScript를 사용하면 컴파일 시점에 오류를 발견하고, 코드 자동 완성과 문서화 혜택을 얻을 수 있습니다.

React 컴포넌트 타입 정의


import React, { useState, useEffect, useRef, ReactNode } from 'react';

// 함수형 컴포넌트의 props 타입 정의
interface ButtonProps {
  text: string;
  onClick: () => void;
  disabled?: boolean;
  variant?: 'primary' | 'secondary' | 'danger';
  size?: 'small' | 'medium' | 'large';
  className?: string;
  children?: ReactNode;
}

// 함수형 컴포넌트
const Button: React.FC<ButtonProps> = ({
                                         text,
                                         onClick,
                                         disabled = false,
                                         variant = 'primary',
                                         size = 'medium',
                                         className = '',
                                         children
                                       }) => {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      className={`button button-${variant} button-${size} ${className}`}
    >
      {text}
      {children}
    </button>
  );
};

// 제네릭 컴포넌트
interface SelectProps<T> {
  items: T[];
  selectedItem: T | null;
  onSelect: (item: T) => void;
  renderItem: (item: T) => ReactNode;
  getKey: (item: T) => string | number;
}

function Select<T>({
                     items,
                     selectedItem,
                     onSelect,
                     renderItem,
                     getKey
                   }: SelectProps<T>) {
  return (
    <ul className="select">
      {items.map(item => (
        <li
          key={getKey(item)}
          className={item === selectedItem ? 'selected' : ''}
          onClick={() => onSelect(item)}
        >
          {renderItem(item)}
        </li>
      ))}
    </ul>
  );
}

// 상태와 이벤트 핸들러 타입
interface User {
  id: number;
  name: string;
  email: string;
}

const UserProfile: React.FC<{ userId: number }> = ({ userId }) => {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState<boolean>(true);
  const [error, setError] = useState<string | null>(null);

  // useRef 타입
  const prevUserIdRef = useRef<number>();

  // 이벤트 핸들러 타입
  const handleButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    console.log('Button clicked', event.currentTarget);
  };

  const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    console.log('Input changed', event.target.value);
  };

  const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    console.log('Form submitted');
  };

  // useEffect
  useEffect(() => {
    const fetchUser = async () => {
      try {
        setLoading(true);
        setError(null);

        // API 호출 시뮬레이션
        const response = await fetch(`https://api.example.com/users/${userId}`);

        if (!response.ok) {
          throw new Error('Failed to fetch user');
        }

        const data: User = await response.json();
        setUser(data);
      } catch (err) {
        setError(err instanceof Error ? err.message : 'An unknown error occurred');
      } finally {
        setLoading(false);
      }
    };

    fetchUser();

    // 이전 userId 저장
    prevUserIdRef.current = userId;
  }, [userId]);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>No user found</div>;

  return (
    <div className="user-profile">
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>

      <form onSubmit={handleFormSubmit}>
        <input
          type="text"
          value={user.name}
          onChange={handleInputChange}
        />
        <button type="submit" onClick={handleButtonClick}>
          Update Profile
        </button>
      </form>
    </div>
  );
};

// 클래스 컴포넌트 타입
interface CounterProps {
  initialCount: number;
  step?: number;
}

interface CounterState {
  count: number;
  lastUpdated: Date | null;
}

class Counter extends React.Component<CounterProps, CounterState> {
  // 기본 props 정의
  static defaultProps: Partial<CounterProps> = {
    step: 1
  };

  constructor(props: CounterProps) {
    super(props);
    this.state = {
      count: props.initialCount,
      lastUpdated: null
    };
  }

  increment = () => {
    this.setState(prevState => ({
      count: prevState.count + (this.props.step || 1),
      lastUpdated: new Date()
    }));
  };

  decrement = () => {
    this.setState(prevState => ({
      count: prevState.count - (this.props.step || 1),
      lastUpdated: new Date()
    }));
  };

  render() {
    return (
      <div>
        <h2>Count: {this.state.count}</h2>
        {this.state.lastUpdated && (
          <p>Last updated: {this.state.lastUpdated.toLocaleTimeString()}</p>
        )}
        <button onClick={this.increment}>+</button>
        <button onClick={this.decrement}>-</button>
      </div>
    );
  }
}

// 고차 컴포넌트(HOC) 타입
interface WithLoadingProps {
  loading: boolean;
}

// HOC 함수
function withLoading<P extends object>(
  Component: React.ComponentType<P>
): React.FC<P & WithLoadingProps> {
  return ({ loading, ...props }: WithLoadingProps & P) => {
    if (loading) return <div>Loading...</div>;
    return <Component {...props as P} />;
  };
}

// HOC 사용
const UserListWithLoading = withLoading(
  ({ users }: { users: User[] }) => (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
);

// 사용 예시
const App: React.FC = () => {
  const [users, setUsers] = useState<User[]>([]);
  const [loading, setLoading] = useState<boolean>(true);

  useEffect(() => {
    // 데이터 로딩 시뮬레이션
    setTimeout(() => {
      setUsers([
        { id: 1, name: 'John Doe', email: 'john@example.com' },
        { id: 2, name: 'Jane Smith', email: 'jane@example.com' }
      ]);
      setLoading(false);
    }, 2000);
  }, []);

  return <UserListWithLoading users={users} loading={loading} />;
};

// 컨텍스트 API 타입
interface ThemeContextType {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

// 기본값 생성 (타입 체크를 위한 더미 함수 포함)
const defaultThemeContext: ThemeContextType = {
  theme: 'light',
  toggleTheme: () => {}
};

const ThemeContext = React.createContext<ThemeContextType>(defaultThemeContext);

const ThemeProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
  const [theme, setTheme] = useState<'light' | 'dark'>('light');

  const toggleTheme = () => {
    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  const value = { theme, toggleTheme };

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
};

// 컨텍스트 사용
const ThemedButton: React.FC<{ text: string }> = ({ text }) => {
  const { theme, toggleTheme } = React.useContext(ThemeContext);

  return (
    <button
      onClick={toggleTheme}
      style={{
        backgroundColor: theme === 'light' ? '#fff' : '#333',
        color: theme === 'light' ? '#333' : '#fff',
        border: `1px solid ${theme === 'light' ? '#333' : '#fff'}`
      }}
    >
      {text} (Theme: {theme})
    </button>
  );
};

// 폼 이벤트 처리
interface LoginFormState {
  email: string;
  password: string;
}

const LoginForm: React.FC<{ onSubmit: (data: LoginFormState) => void }> = ({ onSubmit }) => {
  const [formData, setFormData] = useState<LoginFormState>({
    email: '',
    password: ''
  });

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { name, value } = e.target;
    setFormData(prev => ({
      ...prev,
      [name]: value
    }));
  };

  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    onSubmit(formData);
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="email">Email:</label>
        <input
          type="email"
          id="email"
          name="email"
          value={formData.email}
          onChange={handleChange}
          required
        />
      </div>
      <div>
        <label htmlFor="password">Password:</label>
        <input
          type="password"
          id="password"
          name="password"
          value={formData.password}
          onChange={handleChange}
          required
        />
      </div>
      <button type="submit">Login</button>
    </form>
  );
};

// 타입스크립트와 React Hooks
function useCounter(initialValue: number = 0, step: number = 1) {
  const [count, setCount] = useState<number>(initialValue);

  const increment = () => setCount(c => c + step);
  const decrement = () => setCount(c => c - step);
  const reset = () => setCount(initialValue);

  return { count, increment, decrement, reset };
}

// 제네릭 커스텀 Hook
function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T) => void] {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch (error) {
      console.error(error);
      return initialValue;
    }
  });

  const setValue = (value: T) => {
    try {
      setStoredValue(value);
      window.localStorage.setItem(key, JSON.stringify(value));
    } catch (error) {
      console.error(error);
    }
  };

  return [storedValue, setValue];
}

// 타입스크립트와 React Router
interface RouteParams {
  id: string;
}

interface UserDetailProps {
  match: {
    params: RouteParams;
  };
}

const UserDetail: React.FC<UserDetailProps> = ({ match }) => {
  const userId = parseInt(match.params.id, 10);

  // 사용자 데이터 로딩 로직...

  return <div>User ID: {userId}</div>;
};

// 타입스크립트와 Redux
interface AppState {
  counter: {
    value: number;
  };
  user: {
    data: User | null;
    loading: boolean;
    error: string | null;
  };
}

// 액션 타입
enum ActionType {
  INCREMENT = 'INCREMENT',
  DECREMENT = 'DECREMENT',
  FETCH_USER_REQUEST = 'FETCH_USER_REQUEST',
  FETCH_USER_SUCCESS = 'FETCH_USER_SUCCESS',
  FETCH_USER_FAILURE = 'FETCH_USER_FAILURE'
}

// 액션 인터페이스
interface IncrementAction {
  type: ActionType.INCREMENT;
  payload: number;
}

interface DecrementAction {
  type: ActionType.DECREMENT;
  payload: number;
}

interface FetchUserRequestAction {
  type: ActionType.FETCH_USER_REQUEST;
}

interface FetchUserSuccessAction {
  type: ActionType.FETCH_USER_SUCCESS;
  payload: User;
}

interface FetchUserFailureAction {
  type: ActionType.FETCH_USER_FAILURE;
  payload: string;
}

// 유니온 타입으로 모든 액션 타입 정의
type CounterAction = IncrementAction | DecrementAction;
type UserAction = FetchUserRequestAction | FetchUserSuccessAction | FetchUserFailureAction;
type AppAction = CounterAction | UserAction;

// 액션 생성자
const increment = (amount: number): IncrementAction => ({
  type: ActionType.INCREMENT,
  payload: amount
});

const decrement = (amount: number): DecrementAction => ({
  type: ActionType.DECREMENT,
  payload: amount
});

// 리듀서
const counterReducer = (state = { value: 0 }, action: CounterAction) => {
  switch (action.type) {
    case ActionType.INCREMENT:
      return { ...state, value: state.value + action.payload };
    case ActionType.DECREMENT:
      return { ...state, value: state.value - action.payload };
    default:
      return state;
  }
};

// 타입스크립트와 styled-components
import styled from 'styled-components';

interface ButtonStyleProps {
  primary?: boolean;
  size?: 'small' | 'medium' | 'large';
  outlined?: boolean;
}

const StyledButton = styled.button<ButtonStyleProps>`
  background-color: ${props => (props.primary ? '#0070f3' : '#fff')};
  color: ${props => (props.primary ? '#fff' : '#0070f3')};
  border: ${props => (props.outlined ? '1px solid #0070f3' : 'none')};
  padding: ${props => {
  switch (props.size) {
    case 'small':
      return '8px 16px';
    case 'large':
      return '16px 32px';
    default:
      return '12px 24px';
  }
}};
  border-radius: 4px;
  font-size: ${props => (props.size === 'large' ? '18px' : '16px')};
  cursor: pointer;
  
  &:hover {
    opacity: 0.8;
  }
`;

// 사용 예시
const StyledButtonExample: React.FC = () => {
  return (
    <div>
      <StyledButton>Default Button</StyledButton>
      <StyledButton primary>Primary Button</StyledButton>
      <StyledButton size="large" outlined>
        Large Outlined Button
      </StyledButton>
    </div>
  );
};

코멘트: TypeScript와 React를 함께 사용하면 타입 안전성과 개발자 경험이 크게 향상됩니다. 주요 패턴은 다음과 같습니다:

  1. 컴포넌트 Props 타입 정의:
    • 인터페이스나 타입 별칭을 사용하여 props 타입 정의
    • 선택적 속성(?)과 기본값 활용
    • React.FC<Props> 또는 함수 매개변수에 직접 타입 지정
  2. 이벤트 처리:
    • React.MouseEvent, React.ChangeEvent 등의 제네릭 타입 사용
    • 이벤트 핸들러 함수에 적절한 타입 지정
  3. Hooks 타입 지정:
    • useState<T>, useRef<T> 등에 제네릭 타입 사용
    • 커스텀 Hook에 타입 안전성 추가
  4. 컨텍스트 API:
    • 컨텍스트 값과 Provider에 타입 지정
    • 기본값에 더미 함수 포함하여 타입 체크 지원
  5. 고급 패턴:
    • 제네릭 컴포넌트로 재사용성 높이기
    • 고차 컴포넌트(HOC)에 타입 지정
    • 조건부 타입과 유틸리티 타입 활용

TypeScript를 사용하면 컴파일 시점에 많은 오류를 잡을 수 있고, 자동 완성과 타입 추론을 통해 개발 생산성이 향상됩니다.

Categories:

Updated:

Leave a comment