https://github.com/KimCookieYa/use-react
배경
React 프론트엔드를 구현할 때 자주 구현해야 하는 기능은 Data Fetch이다. 일반적으로 서버에서 받아온 데이터를 보여주기 위해서는 State로 관리해야 하기 때문에, 데이터를 저장할 useState를 선언하고, 데이터를 페치하는 useEffect도 선언해야 한다.
import { useEffect, useState } from 'react';
export default function Exampele() {
const [data, setData] = useState(null);
// useEffect 내에서 REST api 호출 후 state로 저장한다.
useEffect(() => {
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then((response) => response.json())
.then((data) => setData(data));
}, []);
return <div>Example1</div>;
}
주로 위와 같은 코드로 작성하거나 또는 react-query와 같은 비동기 데이터를 관리하기 쉽게 해주는 라이브러리를 사용한다. 다만 코드베이스가 복잡해지면서 애매해졌다.
문제점
- 한 페이지의 로직이 복잡해지면서 호출해야 할 REST api도 늘어났는데, 그럴수록 useEffect 훅이 비례하게 늘어나서 코드양이 확연하게 늘어났다.
- react-query로 전환하자니, 페이지 호출 시에 한 번만 호출하면 되는 api들이 대부분인데다가 굳이 전역상태로 저장할 필요없는 데이터이다.
useFetch
react-query처럼 REST api 호출을 하나의 훅으로 정의할 수 있으면 편할 것 같다는 생각에 useFetch 를 구현하게 되었다. 이미 익숙한 react-query와 같은 DX를 가져갈 수 있도록 props도 유사하게 가져간다.
import { useCallback, useEffect, useRef, useState } from 'react';
interface IUseFetchState<T> {
data: T | null;
isLoading: boolean;
isSuccess: boolean;
isError: boolean;
error: Error | null;
}
interface IUseFetchAction {
refetch: () => void;
}
interface IUseFetchProps<T> {
fetchFn: () => Promise<T>;
initialData?: T | null;
deps?: unknown[];
enabled?: boolean;
successFn?: (data: T) => void | Promise<void>;
errorFn?: (error: Error) => void | Promise<void>;
refetchOnWindowFocus?: boolean;
}
export default function useFetch<T = unknown>({
fetchFn,
initialData = null,
deps = [],
enabled = true,
successFn,
errorFn,
refetchOnWindowFocus = false,
}: IUseFetchProps<T>): IUseFetchState<T> & IUseFetchAction {
// 진행 중인 요청의 취소 여부를 관리할 플래그 ref
const cancelFlagRef = useRef<{ canceled: boolean } | null>(null);
const [state, setState] = useState<IUseFetchState<T>>({
data: initialData,
isLoading: false,
isSuccess: false,
isError: false,
error: null,
});
const fetchData = useCallback(async () => {
// 새로운 요청을 시작하기 전에 이전 요청이 있다면 취소 처리
if (cancelFlagRef.current) {
cancelFlagRef.current.canceled = true;
}
// 현재 요청에 대한 취소 플래그 생성
const cancelFlag = { canceled: false };
cancelFlagRef.current = cancelFlag;
setState((prev) => ({ ...prev, isLoading: true, isError: false }));
try {
const data = await fetchFn();
// 요청 도중에 취소된 경우 결과를 무시합니다.
if (cancelFlag.canceled) return;
setState({
data,
isLoading: false,
isSuccess: true,
isError: false,
error: null,
});
if (successFn) {
successFn(data);
}
} catch (error) {
if (cancelFlag.canceled) return;
const err = error instanceof Error ? error : new Error(String(error));
setState((prev) => ({
...prev,
isLoading: false,
isSuccess: false,
isError: true,
error: err,
}));
if (errorFn) {
errorFn(err);
}
}
}, [initialData, fetchFn, successFn, errorFn]);
useEffect(() => {
if (!enabled) {
setState((prev) => ({
...prev,
isLoading: false,
isSuccess: false,
isError: false,
error: null,
}));
return;
}
fetchData();
return () => {
if (cancelFlagRef.current) {
cancelFlagRef.current.canceled = true;
}
};
}, [enabled, fetchData, ...deps]);
useEffect(() => {
if (refetchOnWindowFocus) {
const onFocus = () => {
if (enabled) {
fetchData();
}
};
window.addEventListener('focus', onFocus);
return () => {
window.removeEventListener('focus', onFocus);
};
}
}, [enabled, fetchData, refetchOnWindowFocus]);
return {
...state,
refetch() {
if (enabled) {
fetchData();
}
},
};
}
- fetch api 또는 axios 중에서 자유롭게 사용가능하도록 Promise<T>를 반환하는 fetchFn 함수를 인자로 받는다.
- cancelFlag 대신 컴포넌트 마운트 여부에 따라 fetch 요청을 제어할 수 있도록 AbortController를 추가할 수도 있을 것 같다.
- 에러 핸들링 로직이 부실한 듯하다.
useFetch 사용 예시
import useFetch from '../packages/useFetch';
export default function Exampele({ orderId }: { orderId: number | null }) {
const { data, isLoading, isSuccess, isError, error, refetch } = useFetch({
fetchFn: async () => {
const response = await fetch(
'https://jsonplaceholder.typicode.com/posts/1',
);
const data = await response.json();
return data;
},
enabled: orderId !== null,
});
return <div>Order ID: {orderId}</div>;
}
'IT > Front-End' 카테고리의 다른 글
[FE] eslint & prettier에서 Biome으로 마이그레이션하기 (0) | 2025.02.18 |
---|---|
[리팩토링 2판] 11장 API 리팩터링 - 일급 함수와 명령 객체 (0) | 2024.12.01 |
프론트엔드 테스팅을 해야겠다 (0) | 2024.11.29 |
[리팩토링 2판] 7장 캡슐화 (1) | 2024.11.03 |
리팩토링 - 장대한 시작.. (0) | 2024.10.15 |