React Query는 React Application에서 서버의 상태를 불러오고, 캐싱하며, 지속적으로 동기화하고 업데이트 하는 작업을 도와주는 라이브러리이다.
프로젝트에서 조금 더 규격화된 API 요청 방식 고려와 useEffect 사용을 줄여 복잡한 코드구조를 개선하기 위해 사용하게 되었다.
npm install react-query
리액트 쿼리 개발자 도구를 세팅한다.
개발자 도구를 통해 캐시로 저장하는 useQuery 데이터를 작업하면서 실시간으로 볼 수 있다.
import { ReactQueryDevtools } from 'react-query/devtools'
실행시 브라우저에서 위와 같이 하단바에 개발자 도구가 나타나는 것을 확인할 수 있다.
React Query 라이브러리에서의 useQuery 훅은 API 요청을 처리하고 상태를 관리하는 기능을 제공한다.
이를 통해 데이터를 가져오고(GET
), 캐싱하며, 상태 변화에 따라 자동으로 업데이트할 수 있다.
데이터를 가져오는 것 뿐만 아니라, 데이터 요청 로딩 상태까지 받아올 수 있는 것이 장점이다.
이 점을 활용하여 useQuery로 API GET
요청을 보내 상품 데이터를 받아오는 커스텀 훅을 프로젝트에 적용하였다.
내 코드의 경우 데이터 요청에 성공했을 때에만 데이터를 변경해 return하고자 useState로 관리하는 productData
를 정의해 두었다.
export const useGetProduct = (product_id: number) => {
const [productData, setProductData] = useState<ProductRes>(initialProductState as ProductRes);
/* ... */
}
const getProduct = async (product_id: number): Promise<ProductRes> => {
const res = await urlInstance.get(`/products/${product_id}/`); // 커스텀한 axios 인스턴스를 통해 API 요청
return res.data;
};
}
useGetProduct
훅 내 GET 요청을 보내 상품 데이터를 가져오는 async/await 함수를 생성한다.
const { isLoading } = useQuery(['product', product_id], () => getProduct(product_id), {
onSuccess: (data) => {
setProductData(data);
},
});
return {
productData,
isProductLoading: isLoading,
};
useQuery 훅을 사용하여 API 요청을 처리한다.
첫번째 인자에 ['product', product_id]
라는 쿼리 키를 정의해서 캐싱을 위해 사용되도록 한다.
두번째 인자로는 getProduct
함수를 전달하여 실제 API 요청을 수행한다.
onSuccess
옵션을 설정하여 요청이 성공할 경우에만 setProductData
를 통해 상태를 업데이트한다. (그 외 onError 등 요청 결과에 대한 다양한 옵션 설정 가능)
마지막으로, productData
와 isLoading
가 담긴 객체를 반환한다. productData는 가져온 상품 데이터를 담고 있으며, isLoading
은 API 요청이 진행 중인지를 나타내는 상태값이다.
// useGetProduct
import { useQuery} from 'react-query';
import { axiosInstance, imgInstance, urlInstance, userInstance } from 'src/api/axiosInstance';
/* ... */
export const useGetProduct = (product_id: number) => {
const [productData, setProductData] = useState<ProductRes>(initialProductState as ProductRes);
const getProduct = async (product_id: number): Promise<ProductRes> => {
const res = await urlInstance.get(`/products/${product_id}/`); // 커스텀한 axios 인스턴스를 통해 API 요청
return res.data;
};
const { isLoading } = useQuery(['product', product_id], () => getProduct(product_id), {
onSuccess: (data) => {
setProductData(data);
},
});
return {
productData,
isProductLoading: isLoading,
};
};
import { useGetProduct } from 'src/hooks/useProduct';
/* ... */
const ProductDetailCard = () => {
const { productData, isProductLoading } = useGetProduct(productId);
return (
productData &&
!isProductLoading && (
{/*태그*/}
)
)
}
useMutation 훅은 React Query 라이브러리에서 제공하는 훅으로, 데이터를 생성(POST), 수정(PUT), 삭제(DELETE)하는 API 요청을 처리하고 관리하는 기능을 제공한다.
이를 통해 데이터의 변경 작업을 수행하고, 자동으로 캐싱 및 상태 업데이트를 처리할 수 있다.
다음은 프로젝트에 적용했던 상품 등록을 위한 useMutation 커스텀 훅 코드이다.
import { useMutation, useQueryClient } from 'react-query';
export const usePostProduct = () => {
const queryClient = useQueryClient();
/* ... */
}
queryClient
는 useQueryClient 훅을 사용하여 초기화된 React Query의 queryClient 인스턴스를 가져오는 역할을 한다. 이 인스턴스를 사용하여 쿼리를 무효화(invalidate)할 수 있다.
const addProduct = async (data: ProductReq) => {
const res = await imgInstance.post<ProductReq>(`/products/`, data);
return res.data;
};
async/await를 사용하여 POST 요청을 보내는 addProduct
라는 함수를 usePostProduct
커스텀 훅 내에 만들었다. 마찬가지로 axios instance를 활용했으며, POST 요청을 보내 제품 데이터를 생성하여 응답으로 받은 데이터는 res.data로부터 추출되어 반환된다.
return useMutation(async (data: ProductReq) => addProduct(data), {
onSuccess: () => {
queryClient.invalidateQueries(['product']);
},
onError: (error: any) => {
throw error;
},
});
addProduct
를 전달하여 실제 API 요청을 수행한다. onSuccess
옵션의 콜백함수가 호출된다. useQuery
와 달리 queryClient.invalidateQueries(['product'])
를 호출하여 'product' 쿼리를 무효화한다. (이를 통해 캐시된 'product' 쿼리 데이터가 업데이트되고, 다시 필요한 경우 새로운 데이터를 가져올 수 있다.)onError
옵션의 콜백함수가 호출된다. 여기에서는 에러를 던져 예외 처리를 위임한다.import { useMutation, useQueryClient } from 'react-query';
export const usePostProduct = () => {
const queryClient = useQueryClient();
const addProduct = async (data: ProductReq) => {
const res = await imgInstance.post<ProductReq>(`/products/`, data);
return res.data;
};
return useMutation(async (data: ProductReq) => addProduct(data), {
onSuccess: () => {
queryClient.invalidateQueries(['product']);
},
onError: (error: any) => {
throw error;
},
});
};
import { usePostProduct, ProductReq } from 'src/hooks/useProduct';
/* ... */
const ProductAddForm = () => {
const usePostProductMutate = usePostProduct();
const [inputValues, setInputValues] = useState<ProductReq>({
product_name: '',
image: null,
price: 0,
shipping_method: '',
shipping_fee: 0,
stock: 0,
product_info: '',
});
const handleInputChange = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
) => {
const { name, value } = e.target;
setInputValues((prevInputValues) => ({
...prevInputValues,
[name]: value.trim(),
}));
};
// 저장하기 버튼 클릭시 handleSaveBtnClick 함수 실행
const handleSaveBtnClick = async () => {
try {
const response = await usePostProductMutate.mutateAsync(inputValues); // mutate 설정
if (response) {
alert('상품이 등록되었습니다.');
navigate(`/seller/dashboard`);
} // Post 요청 보내기
} catch (error: any) {
// 예외 메시지를 이용해 모달 타입 설정
console.error(error);
}
};
return(
{/* ... */}
<Button onClick={handleSaveBtnClick} width='200px'>
저장하기
</Button>
{/* ... */}
)