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

배경

프로젝트에 React Hooks를 본격적으로 도입하기 시작했다. 가장 먼저 개선하고 싶었던 부분은 API 호출 로직이었다. 기존에는 각 컴포넌트마다 componentDidMount에서 fetch를 호출하고, loading/error state를 관리하는 보일러플레이트 코드가 반복되고 있었다.

기존 방식의 문제점

class UserProfile extends React.Component {
  state = { data: null, loading: true, error: null };
  
  componentDidMount() {
    fetch('/api/user')
      .then(res => res.json())
      .then(data => this.setState({ data, loading: false }))
      .catch(error => this.setState({ error, loading: false }));
  }
  
  render() {
    const { data, loading, error } = this.state;
    if (loading) return <Spinner />;
    if (error) return <Error message={error.message} />;
    return <div>{data.name}</div>;
  }
}

이런 패턴이 10개 이상의 컴포넌트에 중복되어 있었다.

Custom Hook 구현

function useApi(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;
}

컴포넌트 unmount 시 state 업데이트를 방지하기 위한 cleanup 로직도 추가했다.

사용 예시

function UserProfile() {
  const { data, loading, error } = useApi('/api/user');
  
  if (loading) return <Spinner />;
  if (error) return <Error message={error.message} />;
  return <div>{data.name}</div>;
}

코드가 확실히 간결해졌다. Class 컴포넌트 대비 30% 정도 코드량이 줄었고, 로직 재사용이 가능해졌다.

추가 개선 계획

현재는 GET 요청만 지원하는데, POST/PUT 같은 mutation 로직도 별도 Hook으로 분리할 예정이다. SWR이나 React Query 같은 라이브러리도 검토 중이지만, 프로젝트 규모상 당장은 이 정도면 충분해 보인다.

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