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 로직을 표준화할 예정이다.

React 컴포넌트 재사용을 위한 Render Props 패턴 적용기