React 컴포넌트 재사용을 위한 Render Props 패턴 적용기
문제 상황
프로젝트 내 여러 컴포넌트에서 API 호출 후 로딩/에러 상태를 처리하는 로직이 반복됐다. HOC(Higher Order Component)로 추상화했지만 TypeScript 타입 추론이 제대로 되지 않아 매번 타입 단언을 해야 했다.
const withDataFetching = (Component) => {
return class extends React.Component {
// props 타입이 any로 추론됨
}
}
Render Props 적용
React 공식 문서에서 권장하는 Render Props 패턴을 적용했다. 함수를 props로 받아 렌더링을 위임하는 방식이다.
interface DataFetcherProps<T> {
url: string;
children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode;
}
class DataFetcher<T> extends React.Component<DataFetcherProps<T>, State<T>> {
state = {
data: null,
loading: true,
error: null
};
componentDidMount() {
fetch(this.props.url)
.then(res => res.json())
.then(data => this.setState({ data, loading: false }))
.catch(error => this.setState({ error, loading: false }));
}
render() {
return this.props.children(this.state.data, this.state.loading, this.state.error);
}
}
사용 예시
<DataFetcher<User> url="/api/users/1">
{(user, loading, error) => {
if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <UserProfile user={user} />;
}}
</DataFetcher>
타입 파라미터를 통해 data의 타입이 정확히 추론되고, 각 컴포넌트에서 필요한 렌더링 로직을 자유롭게 구성할 수 있게 됐다.
결과
HOC 대비 타입 안정성이 확보됐고, 로직과 뷰의 분리가 명확해졌다. 다만 중첩이 깊어지면 콜백 지옥이 될 수 있다는 점은 주의해야 한다. 당분간은 이 패턴으로 데이터 fetching 로직을 표준화할 예정이다.