React Hooks 도입 후 Custom Hook으로 API 호출 로직 정리하기

배경

팀 프로젝트에서 React 16.8로 업그레이드하면서 본격적으로 Hooks를 도입하기 시작했다. 기존에는 API 호출을 위해 HOC와 Render Props 패턴을 혼용했는데, 컴포넌트 depth가 깊어지고 props drilling 문제가 심각했다.

기존 코드의 문제

<DataFetcher url="/api/users">
  {({ data, loading, error }) => (
    <WithAuth>
      {({ user }) => (
        <UserList data={data} loading={loading} />
      )}
    </WithAuth>
  )}
</DataFetcher>

중첩이 심하고, 로직 재사용이 어려웠다.

useFetch Custom Hook

function useFetch(url) {
  const [state, setState] = useState({
    data: null,
    loading: true,
    error: null
  });

  useEffect(() => {
    let cancelled = false;
    
    fetch(url)
      .then(res => res.json())
      .then(data => {
        if (!cancelled) {
          setState({ data, loading: false, error: null });
        }
      })
      .catch(error => {
        if (!cancelled) {
          setState({ data: null, loading: false, error });
        }
      });

    return () => {
      cancelled = true;
    };
  }, [url]);

  return state;
}

개선된 컴포넌트

function UserList() {
  const { data, loading, error } = useFetch('/api/users');
  
  if (loading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  
  return <List items={data} />;
}

훨씬 간결해졌다. cleanup 로직도 useEffect의 return으로 자연스럽게 처리된다.

추가 개선 포인트

  • 재시도 로직을 추가한 useRetryFetch
  • 캐싱이 필요한 경우 useRef로 메모이제이션
  • AbortController를 사용한 요청 취소

Hooks는 확실히 로직 재사용 측면에서 HOC보다 직관적이다. 다만 useEffect의 dependency array 관리에 주의가 필요하다. ESLint의 exhaustive-deps 규칙을 켜두는 게 도움이 됐다.

React Hooks 도입 후 Custom Hook으로 API 호출 로직 정리하기