[React.js] React Query

aeong98·2022년 4월 11일
13
post-thumbnail

Next.js 에 React Query 를 도입하며, 관련된 사용법과 옵션을 기록하기 위한 글입니다.

💁 React Query의 장점

  • ✅ 서버 데이터 캐싱
  • ✅ 데이터 패칭 시 로딩, 에러 처리를 한 곳에서 처리 가능
  • ✅ prefetching, retry 등 다양한 옵션
  • ✅ 쉬운 상태관리

♻️ React Query 의 라이프 사이클

라이프 사이클 별 상태 개념

  • fetching : 데이터 요청 상태
  • fresh : 데이터가 프레시한 (만료되지 않은 상태)
    • 컴포넌트의 상태가 변경되더라도, 데이터를 다시 요청하지 않는다.
    • 새로고침하면 다시 fetching 한다.
  • stale : 데이터가 만료된 상태.
    • 한번 프론트로 내려준 서버 데이터는, 최신화가 필요한 데이터라고 볼 수 있다.
    • 그 사이에 다른 유저가 데이터를 추가, 수정, 삭제 등을 할 수 있기 때문에
    • 컴포넌트가 마운트, 업데이트되면 데이터를 다시 요청한다.
    • fresh 에서 stale 로 넘어가는 시간의 디폴트는 0이다.
  • inactive : 사용하지 않는 상태
    • 일정 시간이 지나면 가비지 콜렉터가 캐시에서 제거한다.
    • 기본값 5분
  • delete : 가비지 콜렉터에 의해 캐시에서 제거된 상태.

라이프 사이클

  1. A 쿼리 인스턴스가 mount 됨
  2. 네트워크에서 데이터를 fetch 하고 A 라는 query key 로 캐싱함
  3. 이 데이터는 fresh 상태에서 staleTime (기본값 0) 이후 stale 상태로 변경됨
  4. A 쿼리 인스턴스가 unmount 됨
  5. 캐시는 cacheTime (기본값 5분) 만큼 유지되다가 가비지 콜렉터로 수집됨
  6. 만일 cacheTime 이 지나기 전에 A 쿼리 인스턴스가 새롭게 mount 되면, fetch 가 실행되고, fresh 한 값을 가져오는 동안 캐시 데이터를 보여줌.

staleTime

  • 데이터가 fresh → stale 상태로 변경되는데 걸리는 시간
  • fresh 상태일때는 쿼리 인스턴스가 새롭게 mount 되어도, 네트워크 fetch 가 일어나지 않는다.
  • 데이터가 한번 fetch 되고 나서 staleTime 이 지나지 않았다면, unmount 후 mount 되어도 fetch 가 일어나지 않는다.
  • 디폴트는 0

cacheTime

  • 데이터가 inactive 상태일 때, 캐싱된 상태로 남아있는 시간
  • 쿼리 인스턴스가 unmount 되면, 데이터는 inactive 상태로 변경 된다.
  • 캐시는 cacheTime 만큼 유지된다.
  • cacheTime 이 지나면 가비지 콜렉터로 수집된다.
  • cacheTime 이 지나기 전에 쿼리 인스턴스가 다시 마운트 되면, 데이터를 fetch 하는 동안 캐시 데이터를 보여준다.

🤩 useQuery

데이터 fetching 에 쓰이는 Hook. GET 메소드 사용시에 자주 쓰인다.

QueryKey

  • QueryKey 를 기반으로 데이터 캐싱을 관리한다.
  • 문자열 또는 배열로 저장할 수 있다.
// 문자열
useQuery('todos', ...)
// 배열
useQuery(['todos'], ...)
  • 쿼리가 변수에 의존하는 경우에는 QueryKey 에도 해당 변수를 추가해줘야한다.
const { data, isLoading, error } = useQuery(['todos', id], () => axios.get(`http://.../${id}`));

Query Functions

  • useQuery 의 두번째 인자에는 promise를 반환하는 함수를 넣어주어야 한다.
// 방법 1
useQuery('todos', fetchTodos);

// 방법 2
useQuery(['todos', todoId], () => fetchTodoById(todoId));

Query Options

  • enable
    • false 시 자동으로 데이터를 불러오는 것을 막는다.
    • default : true ( 디폴트 : 자동으로 데이터 불러옴)
    • ex) 클릭 이벤트 시에만, 데이터 패칭해오길 바란다면 enable false 로 줘야함.
  • retry
    • false : 데이터 fetch 에 실패해도 재요청하지 않는다.
    • true : 데이터 fetch 시 실패해도 무한으로 재요청한다.
    • number : 데이터 fetch 에 실패하면 number 번 까지만 재요청한다.
  • staleTime : 캐시가 fresh 하다고 인정하는 시간.
    • number : number 밀리세컨즈 후 stale 상태로 처리할 것인지 설정한다. (default : 0)
    • Infinity : 데이터를 영원히 fresh 상태로 취급한다.
  • cacheTime : 메모리에 살아있는 시간
    • number: number millisecond 동안 캐시 데이터가 메모리에 남아있게 됩니다. 이 이후 가비지 컬렉션에서 이 데이터를 처리합니다. (default: 5 60 1000 => 5 min)
    • Infinity: 영원히 메모리에 데이터를 보관합니다.
  • onSuccess (data : TDdata) ⇒ void
    • 데이터 fetch 성공 시 실행되는 콜백
    • 매개변수 data 에 요청받은 데이터가 들어온다.
  • onError : (error : TError) ⇒ void
    • 데이터 fetch 실패시 실행되는 콜백
    • 매개변수 error 에 실패 정보가 담긴 error 가 들어온다.
  • onSettled : (data? TData, error?: TError) ⇒ void
    • 데이터 fetch 성공, 실패와 관계없이 무조건 동작하는 콜백입니다.
  • select : (data : TData) ⇒ unknown
    • 데이터를 가공할 때에 쓰이는 함수.
    • 이 함수에서 리턴한 형태가 응답받은 데이터의 형태가 됩니다.
  • keepPreviousData
    • 새로 fetch 한 데이터를 화면에 나타내기 전까지 기존에 있던 데이터를 계속 화면에 유지할 지 여부를 결정.
  • initialData
    • 캐시된 데이터가 없을 때, 표시할 초기값.
    • 브라우저 로컬 스토리지에 저장해둔 값으로 데이터를 초기화할 때 사용할 수 있을 것.

그외 리턴 값.

  • status
    • idle : 초기 상태
    • loading : 데이터 fetching 중일 때 상태
    • error : 데이터 fetch에 실패한 상태
    • success : 데이터 fetch 에 성공한 상태
  • isFetching : 데이터가 fetch 상태일 때 true, 캐싱 데이터가 있어서 백그라운드에서 fetch 되더라도 true
  • isLoading 캐싱된 데이터가 없을 때 fetch 중에 true
  • data : 응답 받은 데이터
  • error : 실패 정보
  • refetch : 수동으로 데이터 refetch 를 실행하는 함수. stale 이나 cache 같은 설정들이 무시되고, 무조건 다시 데이터를 fetching 한다.

예시

  • 게시물 목록의 만료 시간을 1분으로 설정해서, 유저가 페이지 번호를 1에서 2로 반복해서 바꾸는 등의 행동을 취할 때, API 중복 호출을 방지할 수 있다.
  • 게시물 목록의 만료 시간이 1분으로 설정되어 있는데 어떤 사용자는 게시글을 그 시간 안에 작성하거나, 수정할 수도 있다. 그래서 게시글 작성 후에는 캐시를 강제로 무효화(invalidate) 하여, 목록을 새로고침한다.
const { data, isFetching } = useQuery([page, searchValue], fetcher, {
    staleTime: 60 * 1000,
    keepPreviousData: true,
  })
  • stateTime 1분
  • 1분 안에 호출했던 쿼리 키로 다시 호출한 경우에는 API 를 호출하지 않고, 캐시에 있는 데이터를 다시 가져와 사용

⚙️ useMuation

서버 데이터 업데이트할 때 쓰이는 Hook
데이터 생성/수정/삭제 시 자주 쓰인다.

다음과 같이 post, put 등 서버 데이터에 변경이 발생할 때 사용되는 Hook 이 useMutation 이다.

const mutation = useMutation(newTodo => axios.post('/todos', newTodo))

const handleSubmit = useCallback(
  (newTodo) => {
    mutation.mutate(newTodo)
  },
  [mutation],
)

하지만, useMutation 의 경우 API 콜 후에 서버의 response 를 data 에서 받아올 수 없다는 단점이 있다.

공식문서에서도 useMutation 의 return 값은 undefined | unknown 으로 명시되어 있다. 만약 useMutation 사용 시 reponse 가 필요한 경우라면, mutateAsync 로 얻어올 수 있다.

mutateAsycn 는 Promise 를 return 하게 되고, Promise result 로 response 를 가져올 수 있다.

mutateAsync 로 서버 response 가져오는 방법.

const postTodo = (todo) => { 
  axios.post('/api/data', { todo }) 
}
const createTodo = useMutation(postTodo); 

createTodo.mutateAsync(todo).then((data) => {
   console.log(data);
   // console로 찍은 data가 서버의 response 값입니다.
});

🎈SSR

React Query는 SSR 을 두가지 방식으로 구현할 수 있다. initalData 를 주는 방법과, Hydration 을 하는 방법.

initialData 를 통한 방법에서는 데이터를 명시해주기만 하면 되기 때문에 훨씬 간단하지만, 만약 여러 컴포넌트에서 해당 데이터를 SSR 을 통해 사용자에게 보여준다고 하면 모든 컴포넌트에 initialData 를 넘겨줘야 하는 문제가 있다. 컴포넌트의 뎁스가 깊어질 수록 비효율적이다.

반면 Hydration 을 통한 방법은 SSR 을 할 때, 원하는 쿼리를 prefetch 하고 해당 쿼리를 사용하는 컴포넌트에서는 동일한 키로 useQuery 훅을 호출하기만 하면 ****

// pages/poke.tsx

import Pokemon from '../components/Pokemon'
import { getPoke } from '../api'
import { QueryClient } from 'react-query'
import { dehydrate } from 'react-query/hydration'

const Poke = () => {
  return (
    <Pokemon />
  )
}

export async function getServerSideProps() {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery('poke', 
	async() => {
	  // Next.js 에서 data 안꺼내주면 serializable 에러 뜸.
		const {data} =getPoke();
		return data;
	}, 
	{ staleTime: 1000 })

  return {
    props: {
      dehydratedState: JSON.parse(JSON.stringify(dehydrate(queryClient))),
    }
  }
}

export default Poke

이후로 getServerSideProps 에서 props 로 넘겨준, dehydratedState를 _app 에서 받아 Hydration 으로 내려줄 것이다. 이제 컴포넌트에서 prefetch 에서 사용된 쿼리와 같은 키인 poke 를 사용해, useQuery 훅을 호출하면 된다.

const { data, isFetching } = useQuery('poke', 
    () => getPoke(), 
    {
      staleTime: 1000,
    }
  )

⭐ 만약 custom hook 을 만든다면?

export function useGetWorst10 (startDate:string, endDate:string, workingHour:string){
  return useQuery(["worst10", startDate, endDate, workingHour],
  async()=> { 
   const {data}= await Worst10API.yulkok( startDate, endDate, workingHour);
   return data.results;
  },
    {
      staleTime: 600*1000,
      keepPreviousData:true
    } 
  )
}

👌 useInfiniteQuery

useInfiniteQuery 란 파라미터 값만 변경해, 동일한 useQuery 를 무한정 호출할 때 사용됩니다.

보통 Infinite Scroll 을 구현할 때, 많이 사용된다.

const {
   fetchNextPage,
   fetchPreviousPage,
   hasNextPage,
   hasPreviousPage,
   isFetchingNextPage,
   isFetchingPreviousPage,
   ...result
 } = useInfiniteQuery(queryKey, ({ pageParam = 1 }) => fetchPage(pageParam), {
   ...options,
   getNextPageParam: (lastPage, allPages) => lastPage.nextCursor,
   getPreviousPageParam: (firstPage, allPages) => firstPage.prevCursor,
 })

Option

  • pageParam
    • page 를 지정해준다. 기본값을 1로 한 상태에서 다음 데이터를 불러온다.
  • getNextPageParam (getPreviousPageParam)
    • page 를 1 증가시키는 역할을 한다.

예시코드

const useBlacklistQuery = () => {
  // useInfiniteQuery에서 쓸 함수
  const fetchBlacklist = async ({ pageParam = 1 }) => {
    const response = await axiosInstance.get(
      `/api/---/${pageParam}`,
    );
    const result = response.data;
    // axios로 받아온 데이터를 다음과 같이 변경! 
    
    return {
      result: result.blacklist,
      nextPage: pageParam + 1,
      isLast: result.is_last,
    }; 
  };

  const query = useInfiniteQuery('[blacklist]', fetchBlacklist, {
    getNextPageParam: (lastPage, pages) => {
      if (!lastPage.isLast) return lastPage.nextPage;
      return undefined;
    },
    refetchOnWindowFocus: false,
    refetchOnMount: true,
    refetchOnReconnect: true,
    retry: 1,
  });

  return query;
};

result 의 구조 예시

result : {
	blacklist : {
    	user1 : {
        },
        user2 : {
        },  
    },
	is_last : true, // 마지막 여부
}

👩‍💻결론

🟢 React Query 는 기존 상태관리 라이브러리에서 요구하는 boilerplate 코드를 제거할 수 있다.

  • 보통 Redux를 사용하고 있다면 후속처릴 위해 redux-thunk, redux-observable, redux-saga 등의 미들웨어를 사용해,서버 데이터 요청 액션이 들어왔을 경우, API 를 호출해 redux 상태를 업데이트 하는 방식을 사용한다.
  • 하지만 React Query 는 기본적으로 함수형 컴포넌트 안에서 훅 형태로 사용하며, 굳이 서버 상태를 다른 장소에 저장할 필요가 없다. 전역적으로 가져오는 것 또한 당연히 가능하고 후속처리 또한 편리하다.

🟢 캐싱 & 리프레시

  • React Query 는 staleTime, cacheTime 등 옵션 파라미터를 통해 API 데이터의 만료 시간, 리프레시 간격, 데이터를 캐시에서 유지할 기간, 브라우저 포커스시 데이터 리프레시 여부, 성공 or 에러 콜백 등 다양한 기능을 제어할 수 있다.
  • 클라이언트에서 사용하는 서버 데이터의 종류와 양이 늘어날 경우, 작업 처리량을 줄이고 사용자 경험을 개선하기위해 캐싱은 유용하게 사용될 수 있다.

🔴 코드 구조를 잘 고민해야 한다.

  • Redux에 비해 보일러플레이트가 적고, 기존의 개발 패턴, 선언적인 구문들이 없어졌다.
  • 비동기 로직들이 컴포넌트 별로 분산되어, 프로젝트 설계에 신경쓰지 않았을 경우 추후 관리나 확장이 오히려 어려워지고, Component 에 유착된다던지 아니면 어디서 쓰이고 있는지 파악이 더 힘들어 질 수 도 있다.
  • 따라서, React-Query 사용 방법에 대해 깊이 고민해볼 필요가 있다.

참고

profile
프린이탈출하자

0개의 댓글