5/17 TIL

kind J·2022년 5월 17일
0
post-thumbnail

리액트 쿼리 시작하기


프리온보딩 수업중에 요 코드를 보시고 강사님이 리액트 쿼리가 시급하다고 하셨다.
리액트 쿼리에 대한 지식이 없어서 왜 필요한지도 몰랐다. 리액트 쿼리를 공부해서 위의 코드에 리액트 쿼리를 어떻게 적용하면 좋을지 생각해보자.

일단 강의 중에 나왔던

  • ReactQueryDevtools
  • QueryClient
  • QueryClientProvider
  • useQuery

를 중점으로 분석해보고 코드를 파해쳐보자..!!

React Query

react query 는 서버의 값을 클라이언트에 가져오거나 캐싱, 값, 업데이트, 에러핸들링 등 비동기 과정을 편하게 하는데 사용된다. 기본적으로 리액트 애플리케이션은 내부 구성 요소에서 데이터를 가져오거나 업데이트 하는 방법을 제공하지 않아서 개발자가 직접 데이터를 가져오는 방법을 구축해야한다. React hooks 를 사용하여 컴포넌트 기반의 상태와 효과를 함께 결합하거나 범용적인 상태 관리 라이브러리를 사용하여 앱 전체에 비동기 데이터를 저장 제공하는 것을 뜻한다.

그런데 서버로 부터 값을 가져오거나 업데이트 하는 로직을 store 내부에 두고 개발하다보면 서버와 클라이언트 데이터들이 서로 상호작용하면서 클라이언트 데이터와 서버데이터가 분리되지 못하고 이상한 데이터가 생기는 경우가 있다고 한다. 이것은 서버의 상태가 클라이언트 상태와 완전히 다르기 때문이다.

리액트 쿼리를 사용함으로서 서버와 클라이언트 데이터를 분리한다.

리액트 쿼리의 장점

  • 서버의 상태를 관리하기 위한 최고의 라이브러리이다. 특별한 구성없이 바로 사용할수 있고 커스터마이징을 할 수 있다.
  • 서버 상태의 까다로운 문제점과 장애물을 없애고 극복하며, 앱 데이터가 제어를 시작하기 전에 제어할 수 있다.
  • 캐싱
  • get 을 한 데이터에 대해 update를 하면 자동으로 get 수행
  • 데이터가 오래되었다고 판단되면 다시 get(invalidateQueries)
  • 동일한 데이터를 여러번 요청하면 한번만 요청한다. 옵션에 따라 중복 호출 시간 조절이 가능한다.
  • 무한 스크롤
  • 비동기 과정을 선언적으로 관리
  • react hook 과 사용하는 구조가 비슷

리액트 쿼리 사용하기

create-react-app 으로 리액트 프로젝트를 만들고, react-query 를 설치한다.

$ npx create-react-app my-app 
# cd my-app
$ yarn install 
$ yarn add react-query
$ yarn start

ReactQueryDevtools

React Query의 모든 내부 작동을 시각화하는 데 도움이 되며 문제가 발생하면 디버깅 시간을 절약할 수 있다.

import { ReactQueryDevtools } from 'react-query/devtools'
<ReactQueryDevtools initialIsOpen />

initialIsOpen 속성을 추가 해준 다음 실행시켜보면 화면에 이런 창이 뜬다. 왼쪽 꽃모양을 아이콘을 눌러서 보이고 숨겨지게 할 수 있다.

QueryClientProvider / QueryClient

리액트의 가장 기본이 되는 곳에 react-query 를 사용할 수 있도록 셋팅한다.

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from "./App";
import { QueryClient, QueryClientProvider } from 'react-query'
import { ReactQueryDevtools } from 'react-query/devtools'

const queryClient = new QueryClient();

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render(
  <React.StrictMode>
      <QueryClientProvider client={queryClient}>
        <ReactQueryDevtools />
			<App />
      </QueryClientProvider>
  </React.StrictMode>
)

리액트 쿼리를 사용하려면 QueryClientProvider를 최상단에서 감싸주어야한다.
쿼리 인스턴스를 생성하고 client={queryClient} 를 작성해준다.

useQuery

useQuery 문법
const { data, isLoading, error } = useQuery(queryKey, queryFn, options)

data, isLoading, error 는 주로 사용되는 3가지 리턴값이다.

  • return 값은 api 의 성공여부, 실패여부, api return 값을 포함한 객체이다.
  • useQuery는 비동기로 작동된다. 한개의 컴포넌트에서 여러개의 useQuery 가 있다면 하나가 끝나고 다음 useQuery 가 실행되는 것이 아닌 두개의 useQuery 가 동시에 실행된다. 여러개의 비동기 query 가 있다면 useQueries를 쓰는 것이 좋다.

상세 설명

  • 데이터를 get 하기 위한 api 이다. post 와 update 는 useMutation을 쓴다.
  • 첫번째 파라미터unique key가 들어가고 두번째 파라미터로는 비동기 함수(api 호출 함수 - 프라미스) 가 들어간다.
  • 첫번째 파라미터로 설정한 unique key는 다른 컴포넌트에서도 해당키를 사용하면 호출이 가능하다. 이 키는 내부적으로 refetching, caching, query 공유를 위해 사용된다.
  • unique Key 는 string 또는 배열로 지정할 수 있다.
  • 구분할 수 있는 key 를 부여해서 또 같은 데이터를 요청하지 않도록 할 수 있다. useQuery의 key가 달라지면 곧바로 다시 api 요청을 한다.
// 문자열
useQuery('todos', ...)
// 배열
useQuery(['todos', lat, lon], ...)

첫번째 파라미터를 배열로 넘기면 배열의 0번 값은 string 값으로 다른 컴포넌트에서 부를 값이 들어간다. 배열의 1번 부터의 값들은 query 내부에서 파라미터로 해당 값이 전달된다.

  const { data, isLoading } = useQuery(
    ['getWeatherForecast5DaysApi', lat, lon],
    () => getWeatherForecast5DaysApi({ lat, lon }).then((res) => res.data),
    {
      ...
    }
  )

따라서 쿼리가 변수에 의존하는 경우에는 QueryKey와 해당 변수를 배열로 만들어서 첫번째 파라미터를 줘야한다.

예시)

const { data, isLoading, error } = useQuery(['todos', id], () => axios.get(`http://.../${id}`));
  • 두번째 파라미터는 data 를 resolve 하거나 error 를 뱉는 Promise를 리턴하는 비동기 함수를 넣는다. 이 함수 안에서 에러가 발생하면 자동으로 error 를 reject 해준다.axios 를 사용하면 응답에 에러가 발생하기 때문에 별다른 처리르 해주지 않아도 되지만 fetch 를 쓸경우에는 일일이 throw new Error()를 해줘야 한다.

  • 세번째 파라미터는 옵션들이 들어간다.

  const { data, isLoading } = useQuery(
    ['getWeatherForecast5DaysApi', lat, lon],
    () => getWeatherForecast5DaysApi({ lat, lon }).then((res) => res.data),
    {
      refetchOnWindowFocus: true,
      suspense: true,
      useErrorBoundary: true,
      onError(err) {
        if (isAxiosError(err)) console.log(err)
      },
    }
  )

Query Options

자주 쓰이는 쿼리옵션을 정리해보자.

refetchOnWindowFocus (boolean | "always")

refetchOnWindowFocus 는 데이터가 stale 상태일 경우 윈도우 포커싱이 될 때마다 새로운 데이터를 가져오는 것을 실행하는 옵션이다. 서버와 동기화가 편하다.

리액트 쿼리는 기본적으로 캐시된 데이터stale 한 상태로 여긴다.
stale 이란 최신화가 필요한 데이터라는 의미이다.

다음의 경우 refetch 가 된다.

  • useQuery 가 처음 호출될 때
  • 크롬에서 다른 탭을 눌렀다가 원래 보던 중인 탭을 눌렀을 때
  • 개발자 도구 창을 켜서 보다가 페이지 내부를 다시 클릭했을 때
  • 브라우저 화면을 이탈했다가 다시 포커스 할 때
  • 네트워크가 다시 연결될 때
  • 특별히 설정한 refetch interval 에 의해서 (refetchInterval)

옵션값

  • default 값이 true 이다.
  • always 로 설정하면 항상 윈도우가 포커싱 될 때마다 retfetch 를 실행한다는 의미이다.
  • staleTime 옵션으로 설정한 시간 동안 데이터가 stale 되지 않도록 해서 refetch를 막을 수도 있다.

예제)

const { data: userInfo } = useQuery(
  ['user'],
  getUser,
  {
    refetchOnWindowFocus: true,
    staleTime: 60 * 1000, // 1분
  }
)

useErrorBoundary

 useErrorBoundary: true,

에러 바운더리를 사용한다는 것은 에러가 발생했을 때 외부로 에러를 그대로 전파하고, 외부의 에러 바운더리 컴포넌트가 이것을 처리한다는 의미이다.

예시)

const WeatherChofu = () => {
...

  const { data, isLoading } = useQuery(
    ['getWeatherForecast5DaysApi', lat, lon],
    () => getWeatherForecast5DaysApi({ lat, lon }).then((res) => res.data),
    {
      useErrorBoundary: true,
      onError(err) {
        if (isAxiosError(err)) console.log(err)
      },
    }
  )

  ...
}

suspense

  • React Query의 suspense 모드를 설정하게 되면 useQuery의 status, error 등을 React.Suspense로 대체해줍니다.
  • suspense 모드를 사용하면 쿼리가 병렬로 실행된다.
const WeatherChofu = () => {
...

  const { data, isLoading } = useQuery(
    ['getWeatherForecast5DaysApi', lat, lon],
    () => getWeatherForecast5DaysApi({ lat, lon }).then((res) => res.data),
    {
      suspense: true,
      useErrorBoundary: true,
      onError(err) {
        if (isAxiosError(err)) console.log(err)
      },
    }
  )

...
}
export const getWeatherForecast5DaysApi = (params: Params) =>
  axios.get<IWeatherAPIRes>(`${WEATHER_BASE_URL}/forecast`, {
    params: {
      ...params,
      appid: process.env.REACT_APP_WEATHER_APP_ID,
      units: 'metric',
    },
  })
  • enabled 를 사용하면 uesQuery 를 동기적으로 사용할 수 있다.

Tips

데이터를 리프레쉬 해야할 때

포스트 요청을 하거나 삭제 요청을 했을 때 화면에 보여주는 데이터에 변화를 주어야 한다. 그러나 query 키가 변하지 않으므로 강제 리프레시를 해야 할 필요가 있다.

 // 캐시의 모든 쿼리를 무효화
 queryClient.invalidateQueries() 
 // `todos`로 시작하는 키로 모든 쿼리를 무효화
 queryClient.invalidateQueries(['todos', 'something'])

useQuery를 비동기로 여러개 실행할 경우 (useQueries )

const usersQuery = useQuery("users", fetchUsers);
const teamsQuery = useQuery("teams", fetchTeams);
const projectsQuery = useQuery("projects", fetchProjects);

위의 세 함수 모두 비동기로 실행하는데 세 변수를 개발자는 다 기억해야하고 세 변수에 대한 로딩, 성공, 실패 처리를 모두 해야할경우..

useQueries 사용한다!
promise.all 처럼 useQuery 를 하나로 묶을 수 있는데 그것이 useQueries 이다. promise.all 과 마찬가지로 하나의 배열에 각 쿼리에 대한 상태 값이 객체로 들어온다.

const result = useQueries([
  {
    queryKey: ["getRune", riot.version],
    queryFn: () => api.getRunInfo(riot.version)
  },
  {
    queryKey: ["getSpell", riot.version],
    queryFn: () => api.getSpellInfo(riot.version)
  }
]);

useEffect(() => {
  console.log(result); 
 // [{rune 정보, data: [], isSucces: true ...}, {spell 정보, data: [], isSucces: true ...}]
  const loadingFinishAll = result.some(result => result.isLoading);
  console.log(loadingFinishAll); 
  // loadingFinishAll이 false이면 최종 완료
}, [result]);

Reference

https://react-query.tanstack.com/guides/important-defaults
https://kyounghwan01.github.io/blog/React/react-query/basic/#querycache
https://velog.io/@kimhyo_0218/React-Query-%EB%A6%AC%EC%95%A1%ED%8A%B8-%EC%BF%BC%EB%A6%AC-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0-useQuery

profile
프론트앤드 개발자로 일하고 있는 kind J 입니다.

0개의 댓글