프로그래밍/React

[React] react-query 적용하기 | useQuery, refetch

choar 2022. 6. 22. 18:57
반응형

[React] react-query 적용하기 | useQuery, refetch

 

쇼핑몰 프로젝트에서 데이터 불러오는 방식을 react-query로 변경하고 있다.

기존에는 useState, useEffect를 사용해서 데이터를 불러왔는데, 이를 모두 대체해 좀 더 간결한 코드를 작성할 수 있다.

 

react-query의 useQuery 사용법을 간단히 설명해보도록 하겠다.

import { useQuery } from 'react-query';
const { data, isLoading, error } = useQuery(queryKey, queryFn?, queryOption);

useQuery는 두번째 패러미터로 전달받은 query function을 실행시켜주고 이 query function의 상태에 따른 여러 값을 객체로 리턴해주는 hook이다.

예를 들어, 지금은 useQuery의 결과값에서 비구조화 할당으로 data, isLoading, error를 받아오고 있다.

 

query function은 데이터를 불러오는 비동기 함수로, Promise를 리턴해야 한다.

isLoading은 query function이 실행이 끝나지 않았다면 true, 끝났다면 false가 된다.

data는 query function에서 마지막으로 resolve된 값이다. 따라서 async, await, state를 모두 사용해 Promise의 결과값을 가져오는 방식보다 코드가 간단해진다.

error는 query function에서 에러가 throw된 경우 그 에러가 된다.

 

이 세가지가 가장 자주 쓰이지만 다른 값도 선택적으로 사용할 수 있다. 후술하겠지만 나는 refetch라는 값을 사용해보았다.

useQuery의 첫번째 패러미터는 query key로, unique한 문자열 또는 배열을 사용할 수 있다. 배열을 사용하는 경우, 배열 원소의 값이 바뀌면 queryFn이 재실행된다.

useQuery의 세번째 패러미터는 query option으로, 여러가지 옵션을 추가할 수 있다. 아직까지 사용해보지는 않았다.

 

 

이제 쇼핑몰 프로젝트의 기존 코드를 살펴보자.

장바구니 페이지는 CartPage.js와 연결되어 있다.

CartPage.js에서는 Header, Cart, Footer 컴포넌트를 묶어주기만 했다.

화면과 코드를 살펴보자.

장바구니 화면 (금액 부분은 아직 cart item과 연결하지 않은 상태)

// CartPage.js

const CartPage = () => {
  return (
    <Container>
      <Header />
      <Cart />
      <Footer />
    </Container>
  );
};

export default CartPage;

 

Cart.js에서 실질적인 장바구니 페이지의 내용을 리턴한다.

 

장바구니 페이지에 접근했을 때, 다음과 같은 경우를 고려해야 한다.

(1) 로그인하지 않은 경우 -> 접근 X

(2) 로그인 되어있는 경우

    (2-1) 판매자인 경우 -> 접근 X

    (2-2) 구매자인 경우

        (2-2-1) 로딩 중인 경우 -> 로딩 중 화면 표시

        (2-2-2) 로딩이 끝난 경우 -> cart item들 표시

 

모든 경우를 고려해서 짠 코드를 확인해보자.

 

// Cart.js

const Cart = () => {
  const isSeller = localStorage.getItem('userType') === 'SELLER' ? true : false;
  const isLogined = localStorage.getItem('token');
  const [isLoading, setIsLoading] = useState(true);
  const [cartItems, setCartItems] = useState([]);

  // cart item 데이터를 불러오는 함수. 예외 처리는 alert로만 하고 있다.
  const getCartItems = async () => {
    fetch(`${API_URL}/cart/`, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `JWT ${localStorage.getItem('token')}`,
      },
    })
      .then((res) => {
        if (!res.ok) throw new Error('http 에러');
        return res.json();
      })
      .then((data) => {
        setCartItems(data.results);
        setIsLoading(false);
      })
      .catch((e) => alert(e.message));
  };

  // 렌더링 시 로그인이 되어 있고 판매자가 아니면 cart item 데이터를 불러온다.
  useEffect(() => {
    if (isLogined && !isSeller) getCartItems();
  }, []);

  // (1) 로그인하지 않은 경우, 예외 컴포넌트를 리턴한다.
  if (!isLogined) return <CartNoaccess type={'login'} />;
  // (2) 로그인 되어있는 경우
  // (2-1) 판매자인 경우, 예외 컴포넌트를 리턴한다.
  if (isSeller) return <CartNoaccess type={'seller'} />;

  // (2-2) 구매자인 경우
  // (2-2-1) 로딩중인 경우, 로딩 컴포넌트를 리턴한다.
  if (isLoading) return <Loading />;

  return (
    // (2-2-2) 로딩이 완료된 경우, cart item들을 리턴한다.
    <CartContainer>
      <h2>장바구니</h2>
      <CartHeader />
      {/* cart item이 0개인 경우 예외처리를 해주었다. */}
      {cartItems.length === 0 ? (
        <CartNothing />
      ) : (
        <CartList cartItems={cartItems} getCartItems={getCartItems} />
      )}
    </CartContainer>
  );
};

export default Cart;

CartList 컴포넌트는 cartItems를 받아 map을 돌려 item 하나하나를 렌더링한다.

CartList 컴포넌트에 getCartItems 함수를 전달한 이유는, CartList에 장바구니 아이템을 삭제하는 기능을 하는 함수가 포함되어 있어 아이템 삭제 후 바로 getCartItems 함수를 실행해 cart item을 다시 받아오기 위함이다.

 

state가 최소 2개(isLoading, cartItems)는 있어야 하고,

useEffect로 처음에 fetch를 해줘야 한다.

 

 

react-query를 사용하여 리팩토링한 코드를 확인해보자.

 

// Cart.js

const getCartItems = async () => {
  return fetch(`${API_URL}/cart/`, {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json',
      Authorization: `JWT ${localStorage.getItem('token')}`,
    },
  })
    .then((res) => {
      if (!res.ok) throw new Error('http 에러');
      return res.json();
    })
    .then((data) => data.results);
};

const Cart = () => {
  const isSeller = localStorage.getItem('userType') === 'SELLER' ? true : false;
  const isLogined = localStorage.getItem('token');

  const {
    data: cartItems,
    isLoading,
    error,
    refetch
  } = useQuery('cartItems', getCartItems);

  // (1) 로그인하지 않은 경우
  if (!isLogined) return <CartNoaccess type={'login'} />;
  // (2) 로그인 되어있는 경우
  // (2-1) 판매자인 경우 -> 접근 X
  if (isSeller) return <CartNoaccess type={'seller'} />;
  // (2-2) 구매자인 경우
  // (2-2-1) 로딩중인 경우
  if (isLoading) return <Loading />;
  // (2-2-2) 에러가 발생한 경우 (New!)
  if (error)
    return <ErrorMessage emoji="😭" message={`에러 발생: ${error.message}`} />;

  // (2-2-3) 로딩이 끝난 경우
  return (
    <CartContainer>
      <h2>장바구니</h2>
      <CartHeader />
      {cartItems.length === 0 ? (
        <CartNothing />
      ) : (
        <CartList cartItems={cartItems} refetchCartItems={refetch} />
      )}
    </CartContainer>
  );
};

export default Cart;

다음과 같은 변화가 생긴 것을 확인할 수 있다.

1. useState, useEffect가 필요 없게 됨

2. 에러가 발생한 경우를 추가로 처리해줄 수 있게 됨

3. getCartItems에서 error를 catch하는 코드가 사라짐 (어차피 useQuery로 처리되기 때문)

 

⚠️ 주의! getCartItems 함수에서 원래는 함수 내에 fetch만 있었으나 바뀐 코드에서는 fetch 앞에 return을 꼭 추가해야 한다.

useQuery에서 queryFn은 Promise를 반환해야 하기 때문이다. return을 추가하지 않으면 data는 기본값인 undefined에서 변하지 않을 것이다.

 

🌟 CartList에 getCartItems 대신 refetch를 전달해주었다.

useQuery가 반환하는 값 중 refetch는 수동으로 refetch를 해주는 함수이다.

cart item을 제거한 경우 useQuery가 data가 바뀐 것을 인식하고 refetch를 자동으로 해주긴 하지만, 시간이 오래 걸린다.

그래서 refetch를 전달받아 바로 실행했더니 cart item 제거가 바로 반영되었다.

data, isLoading, error, refetch 말고도 useQuery가 리턴하는 아주 다양한 값들이 있다.

찬찬히 공부해나가며 사용해봐야겠다.

 

🤔 지금 코드의 문제

react-query 사용 전의 경우 로그인하지 않았거나 판매자인 경우 아예 fetch를 진행하지 않는다.

그런데 react-query 사용 후에는 모든 경우 fetch를 진행한다.

if문을 순서대로 잘 배열하면 표면상 문제는 없지만 필요 없는 fetch가 진행된다는 문제가 있다.

useQuery를 선택적으로 실행시키는 방법에 대해 알아봐야겠다.


References

https://react-query.tanstack.com/reference/useQuery

반응형