React Query 파헤치기

jewoo·2022년 4월 28일
16
post-thumbnail
post-custom-banner

React Query란 무엇인가

리액트에서는 상태관리라는 것을 통해서 렌더링을 하게 된다.
말만 어렵지 그냥 변수에 값(데이터) 할당하고 그에 맞게 렌더링 한다는 뜻이다.
useState으로 지정하고 setState로 수정하고 어쩌고지만 사실 그냥 변수에 값 할당하는건데 불변성 등등 때문에 getter, setter처럼 사용하는데 이런 State를 상태라고 한다. (스벨트는 state고 나발이고 직관적으로 변수에 넣던데...)
유저가 true면 이렇게~ false면 저렇게~ 여기서 유저가 상태다.

아니 그래서 리액트 쿼리가 뭐냐면 서버 상태 관리 라이브러리이다. 서버 상태 관리 -> 서버 데이터 관리 -> 서버에서 받아온 데이터 관리 라이브러리. 즉 서버에서 데이터 받아오고 관리하는걸 도와주는 녀석이다. 사실 알게된건 예전에 알았는데 조금 거들떠보고 '그냥 useEffect에 fetch() 호출하면 될 것을 굳이?' 라고 생각하고 접었는데 현 프로젝트에서 사용해서 학습해보니 굉장히 유용하다는 것을 깨달았다.

React Query의 특징

깔끔하다

Redux가 왜 점점 배척되어 가는가?
너무 더럽기 때문이다.
Redux-toolkit? Vuex 따라한거 같은데 그래도 여전히 복잡하다.
그래서 점점 인기는 식어가고 다른 대체재들이 떠오르고 있다.
React Query의 장점중 하나는 사용시 코드가 굉장히 깔끔하다는 것이다.
한번 얼마나 깔끔해지는지 보자.

import { useState, useEffect } from 'react'
import './App.css'
import axios from 'axios';
import { useQuery } from 'react-query';

type PostType = {
  id: string,
  title: string
}

const getPosts = async () => {
  const { data } = await axios.get('http://localhost:4000/posts');
  return data;
}

function App() {
  const { isLoading, isFetching, isError, data } = useQuery<PostType[], unknown>('posts', getPosts);

  if (isLoading) {
    return (
      <div>Loading...</div>
    )
  }

  if (isError) {
    return (
      <div>Error...</div>
    )
  }

  return (
    <div>
      {
        data?.map(post => {
          return (
            <div key={post.id}>
              {
                post.title
              }
            </div>
          )
        })
      }
    </div>
  )
}

export default App

너무 깔끔하지 않은가?
useEffect, useState 등도 안써도 된다.
속이 편안해진다.

Query Key

useQuery<PostType[], any>('posts', getPosts,{});
제네릭 안에 첫번째 인자는 Data 타입, 두번째 인자는 에러 타입이다.
useQuery의 첫번째 인자는 query key값으로 이 값을 통해서 각 query들을 구분한다.
이게 뭔말이냐면 <div key={post.id}></div> 처럼 각 쿼리 키 값을 구분을 해줘야 된다는 말이다.
상품 조회 쿼리처럼 id값에 따라 조회를 한다면 배열로 (['product',productId]); 이렇게 쿼리값을 넣어주면 된다.
세번째 인자는 옵션을 줄 수 있는데 이 옵션이 가장 중요하므로 뒷부분에서 다루겠다.

캐싱

React Query는 처음 데이터를 fetching 해올 때에는 isLoading이 true이다.
말 그대로 로딩중이니까.
하지만 한번 fetching을 해오고 나면 default 값인 5분동안 캐싱을 해놓는데 이 시간동안에는 다시 방문해도 원래 받아온 데이터를 보여주게 된다. 따라서 이때에는 isLoading이 false로 설정해둔 로딩 애니메이션등은 렌더링 되지 않는다.

하지만 이미 캐싱된 데이터가 다시 들어갔을때 만약 바뀌어져 있다면? 걱정할 필요없다. 백그라운드에서 fetching 해오면서(이때는 isFetching이 true) 변한 부분을 업데이트 해준다. 즉 5분가량은 다시 접속시에 캐싱된 데이터 보여주고 그 다음에 백그라운드에서 작업을 거쳐 변한부분을 업데이트 하는 것이다.

옵션

cacheTime

캐싱 시간을 설정해 줄 수 있다.
기본 값은 5분 가량으로 밀리세컨즈로 원하는 시간만큼 설정이 가능하다.
Infinity로 설정하면 한번 받아온 값을 무한대로 캐싱해놓는다.

{
  cacheTime: Infinity
}

staleTime

staleTime을 설정해주면 그 시간만큼 백그라운드에서 data fetching을 하지 않는다.
왜 백그라운드에서 fetching을 하지 않게 설정해주냐고?
유저가 이곳저곳 계속 왔다갔다하는데 그럴때마다 백그라운드에서 네트워크 요청하면 자주 안바뀌는 페이지는 비용낭비일것 아님? 그럴때 사용하면 된다. Default는 0이여서 항상 백그라운드에서 fetching 해오게 설정되어 있다.

{
	staleTime: 6000
}

refetchOnMount

refetchOnMount는 백그라운드에서 fetching 해올지 말지를 정하는 옵션이다.
캐쉬한번 하고 나면 그냥 계속 캐쉬된 값 보여주고 다시 fetching 해오지 않을거면 사용하면 된다.
Default는 true

{
 	refetchOnMount: false 
}

refetchOnWindowFocus

처음 react query 사용할 때 이게 가장 신기했었는데 유저가 페이지에서 포커스를 잃었다가 다시 들어오면 fetching을 다시 해온다.
이게 뭔말이냐면 내가 A라는 웹사이트를 보고 있다가 카톡하려고 알트(커맨드)탭으로 카톡하고 다시 A사이트로 돌아오면 refetching을 시작한다. 새탭을 열어서 그 탭에서 브라우징 하다가 다시 돌아와도 마찬가지다. 그래서 다시 돌아왔을때 데이터가 변경 되있으면 업데이트 된 데이터를 보여준다.
Default는 true

{
 	refetchOnWindowFocus: false 
}

enabled

이 옵션은 useQuery가 useEffect마냥 처음에 무조건 실행할 것인지를 설정하는 옵션이다.
Default는 true로 무조건 실행이 되는데 유저의 클릭이벤트 등에 사용할때 false로 설정 후 refetch를 이용해 사용할 수 있다.

const { isLoading, isError, data, refetch } = useQuery<PostType[], unknown>('posts', getPosts,{
	enabled: false
});

<div onClick={()=>refetch()}>Fetch</div>

onSuccess, onError

각각 api호출 성공, 실패시에 하고싶은 것들을 해줄 수가 있다.

{
 	onError: (err) => {
      console.log(err.message);
      router.push('/error');
    }
}

select

select 옵션을 통해서 각 데이터 변형이 가능하다.

{
 	 select: data => data.reverse()
}

keepPreviousData

아까 말했듯이 query key의 데이터를 처음 fetching할때 isLoading이 true이다.
근데 isLoading 대신에 유저가 다른곳에서 받아온 방금 전 데이터를 그냥 보여주고 fetching해와서 업데이트 하고 싶다면 이 옵션을 사용하면 된다.
아니 이런 옵션을 대체 왜 필요하고 어디서 씀? 이라고 생각할 수 있는데 pagination 클릭했을때 안에 목록만 쓰윽 바꾸고 싶을때 사용하면 된다. 유저가 2페이지 보고 있다가 3페이지를 눌렀을때 로딩 애니메이션을 보여주는게 아니라 2페이지의 데이터를 보여주다가 새롭게 받아온 3페이지의 데이터로 쓰윽 갈아치울때 사용하면 된다. Gmail 같은 느낌 생각하면 될듯.
Default는 false

{
 	keepPreviousData: true 
}

initialData

데이터를 가져올때 초기값을 설정해 줄 수 있다.
이것도 초기값을 왜 설정하지? 라고 생각할 수 있는데 이걸 활용해서 UX를 조금이나마 향상시킬 수 있다.
일단 우리는 특정 페이지에 어떤 값이 들어갈지 유추가 가능한 경우가 있다.
예를 들어 "제 3기 결산공고"를 클릭해서 들어가면 공고 내용이 나올거 아님?
근데 타이틀은 거의 무조건 똑같이 "제 3기 결산공고" 하고 밑에 내용이 나올테니까 이런경우에 initialData를 설정해 줄 수 있다. 이커머스 상품페이지도 마찬가지일듯.

{
 	initialData: () => {
      useQueryClient().getQueryData('리스트 목록 쿼리 키');
    }
}

useInfiniteQuery

스크롤이 하단에 다다랐을때 우리는 새롭게 fetching을 해와야 할 때가 있다.
인스타 돋보기 처럼 일정부분되면 fetching 해오고 싶을 때 또는 load more 등의 버튼을 눌러서 새롭게 업데이트 할 때이다.
useInfiniteQuery는 사용법이 좀 다르고 약간 복잡해서 추후 정리해서 다시 업데이트 할 예정이다.

useMutation

앞에서는 데이터를 가져오는 방법과 옵션들을 살펴봤는데 데이터를 보내는데에도 사용이 가능하다.
즉 CRUD에서 CUD에도 사용이 가능함.
useMutation을 사용하면 되는데 특이한 점은 query key값이 필요하지 않다는 것이다.

const postUserInfo = async (userInfo: UserType) => {
  try {
    const { data } = await axios.post('http://localhost:4000/user', userInfo);
    return data;
  } catch (err: any) {
    throw new Error(err.response.status);
  }
}

function App() {
  const inputRef = useRef<HTMLInputElement>(null!);
  const {mutate, isLoading, isError, error} = useMutation(postUserInfo);
  
  return (
    <div>
      <input type="text" ref={inputRef} />
      <button 
		disabled={isLoading}
		onClick={() => mutate({ name: inputRef.current.value })}
      >
          Send
	  </button>
    </div>
  )
}

너무 간단하다..

유용한 Tip

다중호출

여러 API 호출이 필요할 때 어떻게 할까?
그냥 여러개 호출하면 됨.
Alias로 구분해서 사용하면 된다.

const { 
  		isLoading: isPostLoading,
        isError: isPostError, 
        data: postData, 
        error: postError 
	  } = useQuery<PostType[], any>('posts', getPosts);

const { 
  		isLoading: isProductLoading,
  		isError: isProductError,
  		data: productData,
  		error: productError } = useQuery<ProductType[], any>('products', getProducts);

동기적 호출

순서대로 API 호출이 필요할 때는 어떻게 할까?
enabled: data!!로 꼼수를 부리면 된다.
1번 API 호출 성공후 2번 API 해야되면 1번 API 데이터값을 2번 enabled에 넣어준다.

{
  	enabled: postData!!
}

useMutation

useMutation 사용시에 팁이다.
To-Do List를 간단하게 구현한다고 가정했을때 useMutation으로 post후 보낸값을 바로 렌더링하고 싶으면 어떻게 해야할까?

  1. 성공 후 데이터를 다시 받아와 렌더링한다.
    onSuccess에 성공 후 invalidateQueries로 쿼리 무효화를 시켜 다시 fetching을 해오게 한다.
    파라미터로는 고유 쿼리 키값을 적어준다.
    그럼 새롭게 업데이트 된 데이터에 따라 화면이 다시 렌더링 된다.
    const queryClient = useQueryClient();
     const { mutate } = useMutation(postUserInfo, {
        onSuccess: (data) => {
          queryClient.invalidateQueries('users');
        }
     });
  2. 성공 후 데이터의 값을 세팅시킨다.
    1번의 경우도 충분하지만 사실 불필요한 네트워크 비용이 발생하기 때문에 성공했다면 API 재호출이 아닌 성공한 데이터의 값을 세팅하면 된다.
    const queryClient = useQueryClient();
     const { mutate } = useMutation(postUserInfo, {
        onSuccess: (data) => {
          queryClient.setQueryData('posts', (oldQueryData: any) => {
            return [
              ...oldQueryData,
              data
            ]
          })
        }
     });
  3. 성공하든 실패하든 보낸 값을 미리 렌더링하고 성공 여부에 따라 재렌더링한다.
    유저가 입력완료하고 버튼 또는 엔터를 누르면 보낼값을 미리 보여주게 처리 후 결과에 따라 렌더링을 refresh 해주는 형태이다. 에러가 났을 경우 에러전 데이터를 보여주고 API 호출이 성공이든 실패든 무조건 쿼리를 무효화해서 다시 fetching 해와 비교하게 하는 형식인데 이 부분은 아직 이해가 덜되 테스트를 여러번 해보고 추후 업데이트 할 예정이다.

폴더구조

실제 사용시에 폴더 구조에 대해서 알아보자.
보통 폴더를 하나 만들고 관련 API에 맞는 파일을 만든다.
한 파일안에 axios 및 useQuery에 관련된 코드를 넣고 useQuery 결과값을 return한다.
사용시에는 컴포넌트에서 import해서 사용한다.
이렇게 하면 axios 코드따로 useQuery로직 따로 왔다갔다 안해도 되고 import 해오는 파일에서 한눈에 확인이 가능하다.

import { useQuery } from "react-query";
import axios from 'axios';

export const getPosts = async () => {
    try {
        const { data } = await axios.get('http://localhost:4000/posts');
        return data;
    } catch (err) {
        throw new Error(err.response.status);
    }
}

export const useGetPostsData = (onError, onSuccess) => {
    return useQuery('posts', getPosts, {
        onError,
        onSuccess
    })
}
import { useGetPostsData } from './hooks/usePostsData';

export default function App(){
 	const {isLoading, isError, data, error} = useGetPostsData();
}

결론

React Query를 쓰면 코드가 굉장히 간단해지는 것만 해도 충분히 사용 가치가 있다고 생각든다.
뿐만 아니라 여러 옵션들로 특정 기능을 구현해야할때 간편하게 구현할 수 있다는 이점이 있다.

profile
Software Engineer
post-custom-banner

2개의 댓글

comment-user-thumbnail
2022년 12월 16일

Redux를 사용하자니 너무 복잡하고 Redux-toolkit을 사용했는데 나름 괜찮았지만 그래도 여전히 불편한감이 없지 않아 있었는데.. React-query가 대체재로 많이 사용되나 보네요
좋은 글 감사합니다!

답글 달기
comment-user-thumbnail
2024년 6월 3일

@geometry dash This was somewhat satisfactory yet still felt awkward. It seems that React-query is often substituted.

답글 달기