React 컴포넌트와 타입스크립트 통합
React와 타입스크립트를 통합하면 컴포넌트의 타입 안정성을 크게 향상시킬 수 있습니다.
이 절에서는 React 컴포넌트를 타입스크립트로 정의하는 다양한 방법과 Best Practices를 다룹니다.
함수형 컴포넌트 정의
함수형 컴포넌트는 다음과 같이 정의할 수 있습니다.
import React from 'react';
interface GreetingProps {
name: string;
age?: number;
}
const Greeting: React.FC<GreetingProps> = ({ name, age }) => {
return (
<div>
Hello, {name}! {age && `You are ${age} years old.`}
</div>
);
};
// 사용
<Greeting name="Alice" age={30} />
클래스 컴포넌트 정의
클래스 컴포넌트는 다음과 같이 정의할 수 있습니다.
import React, { Component } from 'react';
interface CounterProps {
initialCount: number;
}
interface CounterState {
count: number;
}
class Counter extends Component<CounterProps, CounterState> {
constructor(props: CounterProps) {
super(props);
this.state = {
count: props.initialCount
};
}
increment = () => {
this.setState(prevState => ({ count: prevState.count + 1 }));
}
render() {
return (
<div>
Count: {this.state.count}
<button onClick={this.increment}>Increment</button>
</div>
);
}
}
// 사용
<Counter initialCount={0} />
Props와 State 타입 정의
Props와 State의 타입은 interface나 type을 사용하여 정의할 수 있습니다.
interface User {
name: string;
email: string;
}
interface UserListProps {
users: User[];
onSelectUser: (user: User) => void;
}
const UserList: React.FC<UserListProps> = ({ users, onSelectUser }) => {
// ...
};
Best Practice
- 재사용 가능한 타입은 별도의 파일로 분리
- 가능한 한 구체적인 타입 사용 (any 타입 지양)
- 선택적 props는
?
를 사용하여 명시
제네릭 컴포넌트
제네릭 컴포넌트는 재사용 가능한 컴포넌트를 만들 때 유용합니다.
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={['Apple', 'Banana', 'Orange']}
renderItem={(item) => <span>{item}</span>}
/>
React.FC 타입 사용
React.FC
(Function Component) 타입의 장단점
장점
- children prop이 자동으로 포함됨
- 컴포넌트의 반환 타입이 명시적
단점
- defaultProps와의 통합이 어려움
- 제네릭 컴포넌트 정의가 복잡해짐
대안적 접근
type GreetingProps = {
name: string;
children?: React.ReactNode;
};
const Greeting = ({ name, children }: GreetingProps) => {
return (
<div>
Hello, {name}!
{children}
</div>
);
};
Children Prop 타입 지정
Children prop의 타입은 React.ReactNode
를 사용하여 지정할 수 있습니다.
interface WrapperProps {
children: React.ReactNode;
}
const Wrapper: React.FC<WrapperProps> = ({ children }) => {
return <div className="wrapper">{children}</div>;
};
주의사항
React.ReactNode
는 매우 포괄적인 타입이므로, 가능한 경우 더 구체적인 타입을 사용하는 것이 좋습니다.
이벤트 핸들러 타입 지정
이벤트 핸들러의 타입은 React에서 제공하는 타입을 사용할 수 있습니다.
import React from 'react';
interface ButtonProps {
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
}
const Button: React.FC<ButtonProps> = ({ onClick }) => {
return <button onClick={onClick}>Click me</button>;
};
Synthetic Event 객체 활용
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log(event.target.value);
};
defaultProps와 타입스크립트
defaultProps
는 타입스크립트 3.0 이후부터 권장되지 않습니다. 대신 다음과 같은 방법을 사용할 수 있습니다.
interface GreetingProps {
name: string;
greeting?: string;
}
const Greeting = ({ name, greeting = "Hello" }: GreetingProps) => {
return <div>{greeting}, {name}!</div>;
};
Best Practices와 주의사항
- 명시적 타입 사용 :
any
타입 사용을 최소화하고, 가능한 한 구체적인 타입을 사용합니다. - Props 인터페이스 분리 : 재사용 가능한 props 인터페이스는 별도의 파일로 분리합니다.
- 불변성 유지 :
readonly
修飾子를 사용하여 props의 불변성을 강제합니다.
interface UserProps {
readonly name: string;
readonly age: number;
}
- 타입 추론 활용 : 가능한 경우 타입스크립트의 타입 추론 기능을 활용합니다.
- Union 타입 활용 : 여러 가지 경우의 수가 있는 props는 union 타입을 사용합니다.
type ButtonSize = 'small' | 'medium' | 'large';
interface ButtonProps {
size: ButtonSize;
}
- 제네릭 신중하게 사용 : 필요한 경우에만 제네릭을 사용하고, 과도한 사용은 피합니다.
- 이벤트 핸들러 타입 지정 : 정확한 이벤트 타입을 사용하여 타입 안정성을 높입니다.
- children prop 타입 지정 : 가능한 경우
React.ReactNode
대신 더 구체적인 타입을 사용합니다. - HOC와 타입스크립트 : Higher Order Components를 사용할 때는 제네릭을 활용하여 타입 안정성을 유지합니다.
- 테스트 작성 : 타입스크립트로 작성된 컴포넌트에 대해 적절한 단위 테스트를 작성합니다.
React 컴포넌트를 타입스크립트로 작성하면 컴포넌트의 props와 state에 대한 타입 안정성을 확보할 수 있습니다.
이는 런타임 오류를 줄이고, 코드의 자기 문서화를 가능하게 하며, 더 나은 개발자 경험을 제공합니다.