“모든 웹 애플리케이션의 핵심은 결국 데이터 통신입니다.”
세바스티안이 팀 회의에서 말했다. 그의 말처럼, 서버로부터 데이터를 가져와 화면에 표시하는 것은 프론트엔드 개발의 가장 기본적이고도 반복적인 작업이었다. 그리고 이 비동기 데이터 통신 로직은 언제나 세 가지 상태를 동반했다.
- 로딩 (Loading): 데이터를 요청하고 응답을 기다리는 중인 상태.
- 성공 (Success): 데이터를 성공적으로 받아온 상태.
- 에러 (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 같은 강력한 라이브러리로 자라나, 리액트의 데이터 통신 패러다임을 완전히 바꾸어 놓게 될 터였다.
커스텀 훅의 잠재력은 무한해 보였다. 리액트 팀은 이제, 자신들이 만든 이 새로운 도구가 개발자 커뮤니티의 손에서 어떤 놀라운 창조물들을 낳게 될지, 기대를 품고 지켜보기 시작했다.


