Web/React & Next.js

TypeScript 환경에서 React Query 사용하기

일렁이는코드 2022. 11. 28. 16:40

도입 계기

입사 후 첫 프로젝트에서는 서버에서 받아오는 비동기 데이터들을 redux를 사용하지 않고 state로 관리하였고 그렇게 사용하다 보니 생기는 불편함이 몇 가지 있었습니다.

 

1. 컴포넌트 간에 데이터를 Props로 넘겨주어 사용하는 경우가 다반수였습니다.

2. 하나의 컴포넌트에서 사용된 api가 다른 컴포넌트에서도 필요할 때 api를 요청하는 부분이 중복으로 사용되는 경우가 있었습니다.

3. 비동기데이터가 컴포넌트 마다 흩어져있어 한번에 관리하기 어려웠습니다.

 

새롭게 시작하는 프로젝트에서는 비동기 데이터를 관리하고 싶었고 전역상태관리로만 사용했던 redux 미들웨어를 사용하여 써볼까 했지만 기업들에서react-query를사용하고 있는 추세였기에react-query와을 선택하였습니다. 

 

React Query

React Query는 Server State를 관리하는 라이브러리로 React 프로젝트에서 Server와 Client 사이 비동기 로직들을 손쉽게 다루게 해주는 도구입니다. 공식 문서에서 설명하는 Server State는 아래와 같습니다.

  • Client에서 제어하거나 소유되지 않은 원격의 공간에서 관리되고 유지됨
  • Fetching이나 Updating에 비동기 API가 필요함
  • 다른 사람들과 공유되는 것으로 사용자가 모르는 사이에 변경될 수 있음
  • 신경 쓰지 않는다면 잠재적으로 "out of date"가 될 가능성을 지님

 

React query는 server state를 관리해줄 뿐만 아니라 여러 가지로 프론트엔드 개발자의 작업을 줄여주는 기능 또한 제공합니다.

  • update를 하고 나면 get data를 hook 자체에서 호출할 수 있습니다.
  • 데이터가 오래되었다고 판단되면 다시 데이터를 요청해줍니다.
  • 동일 데이터를 여러 번 요청하는 경우가 있다면 한 번만 요청합니다. 
  • 데이터를 요청하는 과정에서 에러가 난다면 지속적으로 요청합니다.
  • 캐싱된데이터로 인해서 불필요한 API 콜을 줄여줄 수 있습니다.
  • 비동기 과정을 unique key를 사용하여 선언적으로 관리할 수 있습니다.
  • 무한 스크롤에 대한 데이터 처리를 해줍니다.

 

시작하기

npm install react-query

react-query 라이브러리를 설치한 후 index.tsx에 사용할 수 있도록 세팅합니다.

 

// src> index.tsx
import React from 'react';
import { createRoot } from 'react-dom/client';

import { QueryClientProvider, QueryClient } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';

import Router from './Router';

const queryClient = new QueryClient();
const container = document.getElementById('root');
const root = createRoot(container!);

root.render(
  <React.StrictMode>
    <QueryClientProvider client={queryClient}> 
      <ReactQueryDevtools initialIsOpen={false} position="bottom-right" /> //devtools이므로 필수X
      <Router />
    </QueryClientProvider>
  </React.StrictMode>
);

QueryClient : React Query가 Client에서 관리하는 Server State들을 Key를 통해 꺼내서 사용할 수 있습니다.

 

React Query는 API 요청을 Get 방식인 Query와 Post 방식인 Mutation 이렇게 두 가지 유형으로 나뉘어 있습니다.

저희는 카카오페이의 블로그(카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유)를 참고하여 Custom Hook을 만들어 API의 상태를 불러왔습니다.

 

 

useQuery (Get)

// src> Queies> useTagData.ts 
import { useQuery } from 'react-query';
import instance from '../../apis/ApiController';
import { API } from '../../apis/config';

export const QUERY_KEY = ['tagData'];

const fetcher = (): Promise<string[]> => {
  return instance.get(`${API.hasTag}`).then(res => res.data);
};

const useTagData = () => {
  return useQuery<string[]>(QUERY_KEY, fetcher, {
    onError: () => {
      console.error('userData error');
    },
    staleTime: 50000,
  });
};

export default useTagData;

저희는 기존에 axios 인터셉터를 사용하여 에러를 처리하고 있기 때문에 fetcher 함수에 인터셉터를 그대로 사용하였습니다. 

 

useQuery Hook의 사용법

const { data } = useQuery(
  queryKey, // 이 Query의 Unique Key (외부에서도 쓸 수 있으므로 중복되지 않는 이름으로 지정을 해주어야함)
  fetchFn, // api 데이터를 요청, Promise를 Return 하는 함수
  options, // useQuery에서 사용되는 Option 객체
);

 

useQuery를 사용하여 받아온 데이터를 실제 컴포넌트에서 사용하는 법

import React from 'react';
import styled from 'styled-components';
import useTagData from '../../../Queries/Main/useTagData';

export default function Main() {
  const { data: tagData } = useTagData();

  return (
    <Wrap>
      <HashTag>
        {tagData?.map((tag: string, idx: number) => (
          <span key={idx}>{tag}</span>
        ))}
      </HashTag>
    </Wrap>
  );
}

 

 

📌 데이터를 바로 요청하지 않기

위에서 사용한 예제는 로그인 유무에 상관없이 메인에 보이는 데이터 리스트를 get 하는 방법입니다. 로그인 상태에서만 요청할 수 있는 데이터는 페이지 로드 시 바로 요청하면 에러가 나고, 로그인 상태인지를 체크한 후 데이터를 요청해야 합니다. 또한 동기적으로 데이터를 처리해야 할 때, enabled 옵션을 사용합니다.

const fetcher = async (): Promise<Type_User> => {
  return await instance
    .get(`${API.user}`)
    .then(res => res.data);
};

const useUserData = (condition: boolean = true) => {
  return useQuery<Type_User>(['userData'], () => fetcher(), {
    onError: () => {
      console.error('userData error');
    },
    staleTime: 50000,
    enabled: condition,
  });
};

export default useUserData;
const { data:userData } = useUserData(isLogined); //state로 true, false값 보내주기

enabled의 기본값은 true로, true 상태일 때는 데이터를 받아오게 하는 것이고 false값을 주게 되면 true로 변경될 때까지는 query를 실행하지 않습니다.

 

📌 데이터를 새로 불러오기 
글을 쓰거나 지우는 등 업데이트가 일어났을 때, 리스트를 다시 불러와야 하는 경우나 탭이나 버튼을 클릭했을 때 데이터를 새로 요청해야 하는 경우가 있습니다. 이때는 invalidateQueries를 사용하여 데이터를 업데이트시켜줍니다.

import { useQueryClient } from 'react-query';

const queryClient = useQueryClient();
 
const onClickMenu = () => {
 queryClient.invalidateQueries('tabData');
};

invalidateQueries의 인자로는 요청하고자 하는 데이터의 unique key값을 입력하면 됩니다.

 

📌 + 다양한 옵션들

const {
  data,
  dataUpdatedAt,
  error,
  errorUpdatedAt,
  failureCount,
  isError,
  isFetched,
  isFetchedAfterMount,
  isFetching,
  isPaused,
  isLoading,
  isLoadingError,
  isPlaceholderData,
  isPreviousData,
  isRefetchError,
  isRefetching,
  isStale,
  isSuccess,
  refetch,
  remove,
  status,
  fetchStatus,
} = useQuery(queryKey, queryFn?, {
  cacheTime,
  enabled,
  networkMode,
  initialData,
  initialDataUpdatedAt,
  isDataEqual,
  keepPreviousData,
  meta,
  notifyOnChangeProps,
  onError,
  onSettled,
  onSuccess,
  placeholderData,
  queryKeyHashFn,
  refetchInterval,
  refetchIntervalInBackground,
  refetchOnMount,
  refetchOnReconnect,
  refetchOnWindowFocus,
  retry,
  retryOnMount,
  retryDelay,
  select,
  staleTime,
  structuralSharing,
  suspense,
  useErrorBoundary,
})

 

useMutation (Post)

useMutation Hook의 사용법

import { useNavigate } from 'react-router-dom';
import { useMutation, useQueryClient } from 'react-query';
import { Type_EditProfile } from '../types/CommonTypes';
import instance from '../../apis/ApiController';
import { API } from '../../apis/config';

const fetcher = async (data: Type_EditProfile) => {
  return await instance.post(`${API.updateProfile}`, data);
};

const useUpdateUserData = () => {
  const queryClient = useQueryClient();
  const navigate = useNavigate();

  return useMutation(fetcher, {
    onSuccess: () => {
      queryClient.invalidateQueries("userData");
      alert('프로필이 성공적으로 변경되었습니다.');
      navigate('/mypage');
    },
    onError: err => {
      console.error('check nickname error', err);
    },
  });
};

export default useUpdateUserData;
const { mutate } = useMutation(
  mutationFn, // api 데이터를 요청, Promise를 Return 하는 함수
  options, // useMutation에서 사용되는 Option 객체
);

useMutation은 useQuery와 다르게 Unique Key를 필수로 정의하지 않아도 됩니다.

 

실제 컴포넌트에서 useMutation을 요청하는 법

const { mutate } = useUpdateUserData();
  
const clickSave = () => {
   const body = {
        profileImg: profileImg,
        bg: backgroundImg,
        nick: nickVal,
        info: descVal
      };
   mutate(body);
 };

api에 넘겨주어야 하는 body 값을 mutate 선언 시 함께 지정해주면 됩니다.

 

📌 + 다양한 옵션들 

const {
  data,
  error,
  isError,
  isIdle,
  isLoading,
  isPaused,
  isSuccess,
  failureCount,
  failureReason,
  mutate,
  mutateAsync,
  reset,
  status,
} = useMutation({
  mutationFn,
  cacheTime,
  mutationKey,
  networkMode,
  onError,
  onMutate,
  onSettled,
  onSuccess,
  retry,
  retryDelay,
  useErrorBoundary,
  meta
})

mutate(variables, {
  onError,
  onSettled,
  onSuccess,
})

 

 


🤔 새로운 기술의 도입으로 처음에는 두려움이 있었지만 막상 글로만 봤을 때랑 써 봤을 때는 이해되는 체감이 정말 달랐습니다. 생각보다 정말 편리하게 비동기 데이터를 관리할 수 있었고, 코드가 줄어들면서 깔끔하게 한눈에 파악하기 쉬웠습니다. 기본적인 사용법을 먼저 습득 후, react query에서 제공하는 옵션들을 공부한다면 react query를 더 잘 사용할 수 있지 않을까요?

반응형