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

배경

올해 초 React 16.8이 정식 릴리즈되면서 Hooks를 프로덕션에 도입하기 시작했다. 기존 Class 컴포넌트로 작성된 코드를 점진적으로 Function 컴포넌트로 전환하는 중인데, API 호출 로직이 컴포넌트마다 중복되는 문제가 있었다.

문제 상황

각 컴포넌트에서 fetch를 호출할 때마다 loading, error, data 상태를 useState로 선언하고, useEffect에서 API를 호출하는 패턴이 반복됐다. 에러 처리나 cleanup 로직도 매번 작성해야 했다.

const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

useEffect(() => {
  fetch('/api/users')
    .then(res => res.json())
    .then(setData)
    .catch(setError)
    .finally(() => setLoading(false));
}, []);

해결: useFetch Hook

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 <Error message={error.message} />;
  return <ul>{data.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}

개선점

  • 컴포넌트 unmount 시 setState 호출로 인한 메모리 누수 방지
  • 로딩/에러 처리 로직 중앙화
  • 테스트 작성이 용이해짐

다음에는 POST/PUT 같은 mutation 로직도 Custom Hook으로 만들어볼 예정이다. Hooks 패턴이 로직 재사용에 확실히 유용하다는 걸 체감했다.

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