[React 완벽가이드] Section 24: React Query

gonn-i·2024년 9월 5일
0

React 완벽 가이드

목록 보기
18/18
post-thumbnail

React Query 뭐고 왜 쓰지

서버 상태 패칭, 캐싱, 동기화, 업데이트 작업을 하주 쉽게 해줍니다
React Query 공식문서

react querytanstack query로 이름이 바뀐 만큼
(사유 : 범용적인(vue나 svelte 에도 적용) 비동기 상태 관리 라이브러리로 발전),
제목만 react query로 써두고 이하의 것들은tanstack query 라고 지칭하겠다 !!

데이터를 가져오거나, 데이터 상태에 따라서 뭐 다시 리패칭하고, 요청에 대한 상태를 체크하는 건 이미 가능하긴 하다.

그래요 원래도 그건 useEffect나 fetch 로 커버 가능한데요?

라고 말한다면, 라이브러리의 출현에는 다 이유가 있는 법.
(그리고 useEffect는 사실 side Effect 이기 때문에, 패칭이 여러개라면 코드 흐름 파악이 어려울 수 있고, 관리도 어려움 + 성능면에서도 좋지 않다고 알고 있음 )

tanstack query 를 이용한다면 적은 코드로 효율화가 가능하며, 속도 향상이 가능하다. 또한 캐싱 처리도 쉽다고 할 수 있다. 이점은 다음과 같이 정리할 수 있다. 👇

tanstack Query로 로딩상태, 성공/실패, 캐싱, 리패칭, stale 상태 관리를 자동으로 처리

  • tanstack Query 을 통해 얻을 수 있는 이점

💬 캐싱
💬 동일한 데이터에 대해, 중복 요청 제거
(-> 같은 쿼리키로 묶인 경우)
💬 백그라운드에서 'stale(오래된)' 데이터를 업데이트
💬 'stale(오래된)' 시점을 설정 가능
💬 페이지네이션 & 무한 스크롤링 쉽게 구현 가능
💬 구조적 공유를 통해 쿼리 결과 메모

(-> 참조 비교를 통해 불필요한 리랜더링 방지하거나 변경되지 않은 부분을 새롭게 할당하지 않고 유지하여 메모리 사용 최적화)
... 등등


그럼 어떻게 사용하나요

초기세팅 및 기본 설정

1️⃣ 패키지 설치

npm i @tanstack/react-query

2️⃣ 최상위 컴포넌트 Provider로 감싸주기

https.js
queryClient 선언 하기

import { QueryClient } from '@tanstack/react-query';

export const queryClient = new QueryClient();

// ... 여타 요청 코드 생략 

app.js

import { QueryClientProvider } from '@tanstack/react-query';
import { queryClient } from './utils/http.js';

// 라우터 부분 생략 ...

function App() {
  return (
    // Provide the client to your App
    <QueryClientProvider client={queryClient}>
      <RouterProvider router={router} />
    </QueryClientProvider>
  )
}

여기에서 queryClient를 굳이 app.js에서 선언하지 않고 다른곳 (https.js) 에서 선언하고 export 하는 이유가 궁금했다. 그래서 gpt 에게 질문하니 역할 분리의존성 및 중복 코드 제거를 위해서 라는 답변을 받았다. (역시,, 마법이 소라챗둥.. )

(그래서 http에 선언하는 이유)

  • queryClient는 요청과 관련한 상태 관리 객체이다. app.js주로 라우팅이나 전체적인 레이아웃을 담당하는 반면, http.js요청과 관련한 로직을 관리하기 때문에 한곳에서 서버와의 요청과 관련된 모든 로직을 관리하게 된다. 따라서, 관심사의 분리(Separation of Concerns) 원칙에 맞는 구조!!

  • 또한, 여러 컴포넌트에서 queryClient를 사용할 수가 있는데 그때마다 app.js에 대한 의존성이 생기고, 불필요한 중복 코드가 늘어난다. 그래서 app 에서 선언해서 export 안 한다. (app.js를 임포트해서 쓰는 경우는 거의 없으니? html에서 가져다 쓰는거 말고는 )

이제 진짜 사용해보자! 데이터 패칭하기 (GET)

여기서 잠깐 tanstack Query 없이, 그동안 우리가 해왔던 방법

  const [data, setData] = useState(null); // 이벤트 데이터를 저장할 상태
  const [isLoading, setIsLoading] = useState(true); // 로딩 상태를 저장
  const [isError, setIsError] = useState(false); // 에러 상태를 저장
  const [error, setError] = useState(null); // 에러 메시지 상태

  useEffect(() => {
    const fetchData = async () => {
      setIsLoading(true); 
      setIsError(false); 

      try {
        const result = await fetchEvents({ max: 3 }); 
        setData(result); 
      } catch (err) {
          setIsError(true);
          setError(err);
        }
      } finally {
        setIsLoading(false); 
      }
    };

    fetchData(); 
  }, []); 
  

여러개의 상태관리와, 상태를 변경하는 작업이 이루어짐을 알 수 있다.

이제 tanstack Query 사용하여 데이터 받아보자!

  const { data, isPending, isError, error } = useQuery({
    queryKey: ['events', { max: 3 }],
    queryFn: ({ signal, queryKey }) => fetchEvents({ signal, ...queryKey[1] }),
    staleTime: 5000, 
  });

사실상 코드라인 수의 눈에 띄는 차이도 보이며! useEffect 를 사용하지 않았기 때문에 코드의 흐름 파악이 쉬워졌다.

하나씩 뜯어봅시다 useQuery!

  // tanstack query가 하는 일
    // 1) 요청을 관리하는 로직을 제공 (비동기 요청을 관리하고, 상태를 추적)
    // 2) 발생 가능한 오류를 추적하는 역할
    // 3) 데이터를 캐싱하여 중복된 요청을 방지 + 캐싱된 데이터를 최신 상태로 유지하면서 성능을 최적화
    // 단! useQuery는 프로미스를 반환하는 함수를 필요로 함
  const { data, isPending, isError, error } = useQuery({
    queryKey: ['events', { max: 3 }], // queryKey를 통해서 react Query에 데이터를 캐싱,  + 중복된 요청을 막음
    //`queryKey`는 배열 형태로 첫 번째 요소는 주로 쿼리 이름을 의미하고, 두 번째 요소는 추가적인 매개변수를 담을 수 있음
    queryFn: ({ signal, queryKey }) => fetchEvents({ signal, ...queryKey[1] }),
    //`queryFn`은 데이터를 패칭하는 함수 
    // `signal`은 요청 취소를 관리하기 위한 AbortController의 신호
    staleTime: 5000, // 새로운 요청을 다시 보낼 시간 : staleTime은 캐싱된 데이터가 최신 상태로 유지되는 기간을 설정 (해당 ms동안 새로운 패칭을 시도하지 않고, 기존에 캐싱죈 데이터를 사용) / 디폴트는 0임
    gcTime: 30000, // 캐싱된 데이터 보관 시간 : (default : 5m = 300000ms) -> 다른 컴포넌트로 이동해서 해당 쿼리가 더이상 필요해지지 않을때 설정 시간이 지나면 메모리에서 삭제
  });

여타 옵션과 리턴값들 보기 🚪
useQuery 관련 문서

데이터 업데이트 (POST, PUT, DELETE)

tanstack Query로 POST 요청 보내보기!

  const {
    mutate,
    isPending: isEditPending,
    isError: isEditError,
  } = useMutation({
    mutationFn: updateEvent,
    onMutate: async (data) => {
      const newEvent = data.event;
      // 즉시 새로운 요청을 만들어내어 데이터 갱신 (낙관적 업데이트)
      await queryClient.cancelQueries({ queryKey: ['events', id] }); // 진행 중인 쿼리 취소 후 (useQuery로 트리거된 쿼리만을 취소)
      const previousEvent = queryClient.getQueryData(['events', id]); // 백엔드단에서의 오류에 대비하여 이전 쿼리값 저장
      queryClient.setQueryData(['events', id], newEvent); // 특정 쿼리 (첫번쨰 인자)에 자체 데이터 (두번째 인자)를 설정

      return {
        previousEvent,
      }; // 이전 쿼리 값을 리턴함으로써 context로 접근 가능케함
    },
    // 롤백 프로세스
    onError: (error, data, context) => {
      queryClient.setQueryData(['events', id], context.previousEvent);
    },
    // 수정 요청의 성공 여부와 관계없이 시행 (백엔드와 프엔간의 데이터 동기화를 위해 쿼리 무효화하여 갱신)
    onSettled: () => {
      queryClient.invalidateQueries(['events', id]);
    },
  });

  function handleSubmit(formData) {
    mutate({ id, event: formData });
    navigate('../');
  }

단, post나 delete put 은 초기 랜더링시 req를 날리는게 아니라, 버튼을 눌러서 되는 경우이기 때문에 위의 예시와 같이 이벤트핸들러에 전달할 함수에 mutate 함수를 작성해준다.

useMutation

근데 이제 데이터의 변동이 생기면, 이제 요청을 받아올때에도 새로운 데이터를 받아 갱신 작업을 해주어야 하는데 이때 staleTime 만을 이용할때는 새로고침(재랜더링)이 이루어져야만 캐싱 데이터가 변경된다. 이럴때 우리가 써야 할 것은 queryClient.invalidateQueries 이다!

staleTime과 queryClient.invalidateQueries 차이

stale (구식) 상태가 될때, tanstack Query 에서는 서버에서 필요한 순간에 새로운 req 를 보냄으로써 최신의 데이터를 가지도록 한다.

(시간이 땡땡땡 다 되었다고 요청 다시 보내는거 아니란 말, 필요한 순간이라 함은 컴포넌트 마운트나 페이지 랜더링관 같은 상황 )

시간을 기준으로 자동으로 데이터를 관리해주는 staleTime

staleTime은 데이터를 캐싱할때에, 데이터의 유효 신선 (fresh) 시간을 설정한다.
(일정 시간 지나야만 필요에 따라 새로운 요청을 보내고 캐시데이터 갱신)

이때, 이 시간이 지나기 전까지 데이터는 최신한(Fresh) 상태로 여겨지며, 서버에 새로 데이터를 요청하지 않으고 캐시된 데이터를 반환한다.

반면, 설정한 시간이 지나면 데이터는 신선하지 못한 상태 (stale) 라고 간주되어, 다시 데이터를 요청되는 상황에 (페이지가 다시 열리기거나, 컴포넌트가 마운트 되거나 하는 ..~ ) 새로운 요청을 보내 새롭게 캐시한다.

결과적으로는 fresh 한 시간을 보장받기 때문에, 불필요한 요청을 없앰

수동으로 특정 쿼리를 갱신 queryClient.invalidateQueries

필요한 순간에 (새로운 이벤트 추가 or 삭제시) 특정 쿼리를 stale 한 상태로 만들어서, 해당 쿼리를 무효한다. 이후 그 쿼리가 요청될때 강제로 데이터를 최신화한다.
(특정 시점에 상태를 수동으로 관리하여, 최신화)

0개의 댓글