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 패턴이 로직 재사용에 확실히 유용하다는 걸 체감했다.