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

배경

올해 초 React 16.8이 정식 릴리즈되면서 Hooks를 프로덕션에 사용할 수 있게 되었다. 기존 Class 컴포넌트로 작성된 프로젝트를 점진적으로 리팩토링하고 있는데, API 호출 로직이 컴포넌트마다 중복되는 문제가 여전했다.

기존 방식의 문제

각 컴포넌트에서 componentDidMountuseEffect마다 loading, error, data 상태를 반복 선언하고 있었다.

function UserList() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [users, setUsers] = useState([]);

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

  // ...
}

useFetch 커스텀 훅

공통 로직을 분리한 Custom Hook을 만들었다.

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

  useEffect(() => {
    let cancelled = false;

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

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

  return state;
}

컴포넌트 코드가 훨씬 간결해졌다.

function UserList() {
  const { loading, data: users, error } = useFetch('/api/users');

  if (loading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  return <ul>{users.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}

배운 점

  • Custom Hook으로 상태 로직 재사용이 HOC나 Render Props보다 직관적이다
  • cleanup 함수로 언마운트 시 메모리 누수를 방지해야 한다
  • Hook의 의존성 배열 관리가 생각보다 중요하다

다음엔 SWR이나 React Query 같은 라이브러리도 검토해볼 예정이다.

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