프리온보딩 수업중에 요 코드를 보시고 강사님이 리액트 쿼리가 시급하다고 하셨다.
리액트 쿼리에 대한 지식이 없어서 왜 필요한지도 몰랐다. 리액트 쿼리를 공부해서 위의 코드에 리액트 쿼리를 어떻게 적용하면 좋을지 생각해보자.
일단 강의 중에 나왔던
를 중점으로 분석해보고 코드를 파해쳐보자..!!
react query 는 서버의 값을 클라이언트에 가져오거나 캐싱, 값, 업데이트, 에러핸들링 등 비동기 과정을 편하게 하는데 사용된다. 기본적으로 리액트 애플리케이션은 내부 구성 요소에서 데이터를 가져오거나 업데이트 하는 방법을 제공하지 않아서 개발자가 직접 데이터를 가져오는 방법을 구축해야한다. React hooks 를 사용하여 컴포넌트 기반의 상태와 효과를 함께 결합하거나 범용적인 상태 관리 라이브러리를 사용하여 앱 전체에 비동기 데이터를 저장 제공하는 것을 뜻한다.
그런데 서버로 부터 값을 가져오거나 업데이트 하는 로직을 store 내부에 두고 개발하다보면 서버와 클라이언트 데이터들이 서로 상호작용하면서 클라이언트 데이터와 서버데이터가 분리되지 못하고 이상한 데이터가 생기는 경우가 있다고 한다. 이것은 서버의 상태가 클라이언트 상태와 완전히 다르기 때문이다.
리액트 쿼리를 사용함으로서 서버와 클라이언트 데이터를 분리한다.
create-react-app 으로 리액트 프로젝트를 만들고, react-query 를 설치한다.
$ npx create-react-app my-app
# cd my-app
$ yarn install
$ yarn add react-query
$ yarn start
React Query의 모든 내부 작동을 시각화하는 데 도움이 되며 문제가 발생하면 디버깅 시간을 절약할 수 있다.
import { ReactQueryDevtools } from 'react-query/devtools'
<ReactQueryDevtools initialIsOpen />
initialIsOpen 속성을 추가 해준 다음 실행시켜보면 화면에 이런 창이 뜬다. 왼쪽 꽃모양을 아이콘을 눌러서 보이고 숨겨지게 할 수 있다.
리액트의 가장 기본이 되는 곳에 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 문법
const { data, isLoading, error } = useQuery(queryKey, queryFn, options)
data, isLoading, error 는 주로 사용되는 3가지 리턴값이다.
// 문자열
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)
},
}
)
자주 쓰이는 쿼리옵션을 정리해보자.
refetchOnWindowFocus 는 데이터가 stale 상태일 경우 윈도우 포커싱이 될 때마다 새로운 데이터를 가져오는 것을 실행하는 옵션이다. 서버와 동기화가 편하다.
리액트 쿼리는 기본적으로 캐시된 데이터를 stale 한 상태로 여긴다.
stale 이란 최신화가 필요한 데이터라는 의미이다.
다음의 경우 refetch 가 된다.
옵션값
예제)
const { data: userInfo } = useQuery(
['user'],
getUser,
{
refetchOnWindowFocus: true,
staleTime: 60 * 1000, // 1분
}
)
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)
},
}
)
...
}
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',
},
})
포스트 요청을 하거나 삭제 요청을 했을 때 화면에 보여주는 데이터에 변화를 주어야 한다. 그러나 query 키가 변하지 않으므로 강제 리프레시를 해야 할 필요가 있다.
// 캐시의 모든 쿼리를 무효화
queryClient.invalidateQueries()
// `todos`로 시작하는 키로 모든 쿼리를 무효화
queryClient.invalidateQueries(['todos', 'something'])
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]);
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