비동기를 다루는 커스텀 훅: useFetch의 탄생

712025년 10월 25일4

“모든 웹 애플리케이션의 핵심은 결국 데이터 통신입니다.”

세바스티안이 팀 회의에서 말했다. 그의 말처럼, 서버로부터 데이터를 가져와 화면에 표시하는 것은 프론트엔드 개발의 가장 기본적이고도 반복적인 작업이었다. 그리고 이 비동기 데이터 통신 로직은 언제나 세 가지 상태를 동반했다.

  1. 로딩 (Loading): 데이터를 요청하고 응답을 기다리는 중인 상태.
  2. 성공 (Success): 데이터를 성공적으로 받아온 상태.
  3. 에러 (Error): 네트워크 문제나 서버 오류로 데이터를 가져오는 데 실패한 상태.

개발자들은 컴포넌트마다 이 세 가지 상태를 관리하기 위해 useState를 반복적으로 선언하고, useEffect 안에서 fetch API를 호출하는 코드를 거의 똑같이 작성하고 있었다.

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch(`https://api.example.com/users/${userId}`)
      .then(response => response.json())
      .then(data => setUser(data))
      .catch(err => setError(err))
      .finally(() => setLoading(false));
  }, [userId]);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error!</p>;
  // ...
}

이 패턴은 너무나도 흔했기에, 팀은 이것을 커스텀 훅으로 추상화하는 것이 시급한 과제라고 판단했다. 그들은 이 훅의 이름을 useFetch라고 잠정적으로 정했다.

useFetch의 목표는 명확했다.
URL을 인자로 받아, 로딩, 데이터, 에러 상태를 모두 알아서 관리하고, 그 세 가지 값을 반환해주는 것.

댄과 동료들은 useWindowWidth를 만들었던 경험을 바탕으로 useFetch의 프로토타입을 설계하기 시작했다.

import { useState, useEffect } from 'react';

function useFetch(url) {
  // 1. 세 가지 상태를 내부적으로 관리한다.
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // 2. url이 바뀔 때마다 데이터를 다시 fetch하는 효과를 만든다.
  useEffect(() => {
    // 이전 요청의 잔재가 남지 않도록 상태를 초기화한다.
    setLoading(true);
    setData(null);
    setError(null);

    fetch(url)
      .then(response => {
        if (!response.ok) throw new Error('Network response was not ok');
        return response.json();
      })
      .then(fetchedData => {
        setData(fetchedData);
      })
      .catch(err => {
        setError(err);
      })
      .finally(() => {
        setLoading(false);
      });
  }, [url]); // 이 효과는 url에 의존한다.

  // 3. 세 가지 상태를 모두 객체로 묶어 반환한다.
  return { data, loading, error };
}

useFetch 훅 안에는 비동기 데이터 통신의 모든 복잡성이 캡슐화되었다. 상태 관리, API 호출, 성공 및 에러 처리까지.

이제 이 훅을 사용하는 컴포넌트의 코드는 놀랍도록 선언적으로 변했다.

function UserProfile({ userId }) {
  const url = `https://api.example.com/users/${userId}`;
  const { data: user, loading, error } = useFetch(url);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return <h1>{user.name}</h1>;
}

컴포넌트는 더 이상 데이터 통신의 구체적인 과정을 알 필요가 없었다. 그저 useFetch에게 URL을 알려주고, loading, error, data라는 세 가지 상태에 따라 무엇을 보여줄지만 결정하면 되었다.

이것은 HOC나 렌더 프롭으로는 결코 도달할 수 없었던 수준의 간결함과 명료함이었다.
상태 로직 재사용이라는, 그들이 처음 풀려고 했던 바로 그 문제가, 커스텀 훅이라는 도구를 통해 가장 복잡한 시나리오 중 하나인 비동기 통신에서마저 완벽하게 해결되고 있었다.

물론, 이 useFetch 프로토타입은 아직 캐싱(caching), 재검증(revalidation), 중복 요청 제거 같은 고급 기능을 갖추고 있지는 않았다.

하지만 이 작은 실험은, 훅이 데이터 패칭 라이브러리의 미래를 어떻게 바꿀 수 있는지 보여주는 중요한 청사진이 되었다. 이 아이디어의 씨앗은 훗날 오픈소스 커뮤니티의 손에서 SWR이나 React Query 같은 강력한 라이브러리로 자라나, 리액트의 데이터 통신 패러다임을 완전히 바꾸어 놓게 될 터였다.

커스텀 훅의 잠재력은 무한해 보였다. 리액트 팀은 이제, 자신들이 만든 이 새로운 도구가 개발자 커뮤니티의 손에서 어떤 놀라운 창조물들을 낳게 될지, 기대를 품고 지켜보기 시작했다.