icon안동민 개발노트

고급 타입 패턴과 Best Practices


 React와 TypeScript를 함께 사용할 때 적용할 수 있는 고급 타입 패턴들은 코드의 타입 안정성과 재사용성을 크게 향상시킵니다.

 이 절에서는 다양한 고급 패턴과 Best Practices를 살펴봅니다.

조건부 타입을 활용한 동적 Props 정의

 조건부 타입을 사용하여 컴포넌트의 props를 동적으로 정의할 수 있습니다.

type ButtonProps<T extends 'button' | 'submit' | 'reset'> = {
  type: T;
} & (T extends 'button' 
  ? { onClick: () => void } 
  : T extends 'submit' 
    ? { onSubmit: () => void } 
    : {});
 
const Button = <T extends 'button' | 'submit' | 'reset'>({ 
  type, 
  ...props
}: ButtonProps<T>) => (
  <button type={type} {...props}>
    Click me
  </button>
);
 
// 사용
<Button type="button" onClick={() => console.log('Clicked')} />
<Button type="submit" onSubmit={() => console.log('Submitted')} />

컴포넌트 인터페이스 설계

 유니온과 인터섹션 타입을 활용하여 유연하고 타입 안전한 컴포넌트 인터페이스를 설계할 수 있습니다.

 유연하고 재사용 가능한 컴포넌트 인터페이스

type Size = 'small' | 'medium' | 'large';
type Color = 'primary' | 'secondary' | 'danger';
 
interface BaseProps {
  className?: string;
  style?: React.CSSProperties;
}
 
interface ButtonProps extends BaseProps {
  size: Size;
  color: Color;
  onClick: () => void;
}
 
type IconButtonProps = ButtonProps & { icon: React.ReactNode };
 
const Button: React.FC<ButtonProps | IconButtonProps> = (props) => {
  // 구현...
};
 
// 사용
<Button size="medium" color="primary" onClick={() => {}} />
<Button size="small" color="danger" onClick={() => {}} icon={<Icon />} />

타입 가드를 사용한 조건부 렌더링

 타입 안전한 조건부 렌더링

interface UserProps {
  name: string;
  age: number;
}
 
interface AdminProps {
  name: string;
  permissions: string[];
}
 
type ProfileProps = UserProps | AdminProps;
 
const Profile: React.FC<ProfileProps> = (props) => {
  if ('permissions' in props) {
    return <AdminProfile {...props} />;
  } else {
    return <UserProfile {...props} />;
  }
};
 
// 사용
<Profile name="John" age={30} />
<Profile name="Admin" permissions={['read', 'write']} />

제네릭 컴포넌트와 고차 컴포넌트(HOC)

 제네릭 컴포넌트

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
}
 
function List<T>({ items, renderItem }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={index}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}
 
// 사용
<List
  items={[1, 2, 3]}
  renderItem={(item) => <span>{item}</span>}
/>

 타입 안전한 HOC

function withLoading<P extends object>(
  WrappedComponent: React.ComponentType<P>
) {
  return function WithLoading(props: P & { loading: boolean }) {
    if (props.loading) return <div>Loading...</div>;
    return <WrappedComponent {...props} />;
  };
}
 
// 사용
const EnhancedComponent = withLoading(MyComponent);
<EnhancedComponent loading={true} otherProp="value" />

'as const'와 리터럴 타입 활용

 상수와 열거형을 효과적으로 다루기

const SIZES = ['small', 'medium', 'large'] as const;
type Size = typeof SIZES[number];
 
const COLORS = {
  PRIMARY: 'blue',
  SECONDARY: 'gray',
  DANGER: 'red',
} as const;
type Color = typeof COLORS[keyof typeof COLORS];
 
interface ButtonProps {
  size: Size;
  color: Color;
}
 
const Button: React.FC<ButtonProps> = ({ size, color }) => {
  // 구현...
};
 
// 사용
<Button size="medium" color="blue" />

유틸리티 타입 적용

 React 컴포넌트에 유틸리티 타입 적용

interface FullComponentProps {
  title: string;
  content: string;
  onSubmit: () => void;
  onCancel: () => void;
}
 
type PartialComponent = React.FC<Partial<FullComponentProps>>;
type ReadonlyComponent = React.FC<Readonly<FullComponentProps>>;
type PickedComponent = React.FC<Pick<FullComponentProps, 'title' | 'content'>>;
 
const FullComponent: React.FC<FullComponentProps> = (props) => {
  // 구현...
};
 
const PartialComponent: PartialComponent = (props) => {
  // 구현...
};
 
// 사용
<FullComponent title="Title" content="Content" onSubmit={() => {}} onCancel={() => {}} />
<PartialComponent title="Title" />

컴포넌트 테스트와 타입 안전성

 타입스크립트를 활용한 컴포넌트 테스트

import { render, fireEvent } from '@testing-library/react';
 
interface ButtonProps {
  onClick: () => void;
  children: React.ReactNode;
}
 
const Button: React.FC<ButtonProps> = ({ onClick, children }) => (
  <button onClick={onClick}>{children}</button>
);
 
test('Button clicks', () => {
  const handleClick = jest.fn();
  const { getByText } = render(<Button onClick={handleClick}>Click me</Button>);
  fireEvent.click(getByText('Click me'));
  expect(handleClick).toHaveBeenCalledTimes(1);
});

Best Practices와 안티 패턴

 Best Practices

  1. 명시적 타입 사용 : any 타입 사용을 최소화하고 구체적인 타입을 사용합니다.
  2. 제네릭 활용 : 재사용 가능한 컴포넌트와 함수에 제네릭을 사용합니다.
  3. 타입 가드 활용 : 조건부 렌더링 시 타입 가드를 사용하여 타입 안전성을 확보합니다.
  4. 유니온 타입 활용 : 여러 가지 케이스를 다루는 컴포넌트에 유니온 타입을 사용합니다.
  5. 불변성 유지 : readonly 修飾子와 Readonly 유틸리티 타입을 활용합니다.
  6. 타입 추론 활용 : 가능한 경우 TypeScript의 타입 추론 기능을 활용합니다.
  7. 인터페이스 확장 : 공통 속성을 가진 인터페이스는 확장하여 재사용성을 높입니다.
  8. 상수 사용 : 문자열 리터럴 대신 상수를 사용하여 타입 안전성을 높입니다.
  9. 테스트 작성 : 컴포넌트와 함수에 대한 타입 안전한 테스트를 작성합니다.
  10. 문서화 : 복잡한 타입과 컴포넌트에 대해 주석을 통해 문서화합니다.

 안티 패턴

  1. any 타입의 과도한 사용
  2. 타입 단언(as)의 무분별한 사용
  3. 제네릭 컴포넌트의 과도한 복잡성
  4. 불필요한 타입 정의 (타입 추론이 가능한 경우)
  5. 타입 정의 파일(.d.ts)의 부적절한 사용
  6. 비즈니스 로직과 타입 로직의 혼합
  7. 타입 안전성을 위한 과도한 조건문 사용
  8. 타입 정의의 중복
  9. 부적절한 null/undefined 처리
  10. 타입 정의 없이 외부 라이브러리 사용