고급 타입 패턴과 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
- 명시적 타입 사용 :
any
타입 사용을 최소화하고 구체적인 타입을 사용합니다. - 제네릭 활용 : 재사용 가능한 컴포넌트와 함수에 제네릭을 사용합니다.
- 타입 가드 활용 : 조건부 렌더링 시 타입 가드를 사용하여 타입 안전성을 확보합니다.
- 유니온 타입 활용 : 여러 가지 케이스를 다루는 컴포넌트에 유니온 타입을 사용합니다.
- 불변성 유지 :
readonly
修飾子와Readonly
유틸리티 타입을 활용합니다. - 타입 추론 활용 : 가능한 경우 TypeScript의 타입 추론 기능을 활용합니다.
- 인터페이스 확장 : 공통 속성을 가진 인터페이스는 확장하여 재사용성을 높입니다.
- 상수 사용 : 문자열 리터럴 대신 상수를 사용하여 타입 안전성을 높입니다.
- 테스트 작성 : 컴포넌트와 함수에 대한 타입 안전한 테스트를 작성합니다.
- 문서화 : 복잡한 타입과 컴포넌트에 대해 주석을 통해 문서화합니다.
안티 패턴
any
타입의 과도한 사용- 타입 단언(
as
)의 무분별한 사용 - 제네릭 컴포넌트의 과도한 복잡성
- 불필요한 타입 정의 (타입 추론이 가능한 경우)
- 타입 정의 파일(
.d.ts
)의 부적절한 사용 - 비즈니스 로직과 타입 로직의 혼합
- 타입 안전성을 위한 과도한 조건문 사용
- 타입 정의의 중복
- 부적절한
null
/undefined
처리 - 타입 정의 없이 외부 라이브러리 사용