이전의 글에서 우아한테크세미나 강의를 듣고 React Query에 대하여 정리해보는 글을 작성하였다. 복습도 해볼겸, 현재 진행 중인 프로젝트에 react query를 적용시켜, server state 를 관리 해보고자 한다.
현재 Next.js + TypeScript 를 사용하여 진행중인 프로젝트에 서버의 데이터를 가져오기 위하여 React Query 를 적용해 볼 것이다.
먼저 npm i react-query
를 통해 설치를 해준다.
최상단인 파일에 QueryClientProvider 를 필수적으로 해주어야 사용할 수 있다.
참고로, react-query에서 devtools 를 지원한다. devtools는 react-query/devtools 패키지에 포함되어 있어서 추가로 설치할 필요가 없다.
또한, process.env.NODE === 'production' 일 때 프로덕션 번들에 포함되지 않아, 프로덕션 빌드 중에 devtools를 제외시키는 것을 걱정할 필요가 없다.
devtools를 통하여 디버깅을 유연하게 할 수 있다. devtools는<ReactQueryDevtools />
를 코드에 작성하면 확인할 수 있다.
import { QueryClient, QueryClientProvider } from 'react-query'
import { ReactQueryDevtools } from 'react-query/devtools'
import type { AppProps } from 'next/app'
import Layout from '../components/Layout'
export default function App({ Component, pageProps }: AppProps) {
const client = new QueryClient()
return (
<Layout>
<QueryClientProvider client={client}>
<Component {...pageProps} />
<ReactQueryDevtools />
</QueryClientProvider>
</Layout>
)
}
The Movie Database API 를 이용하여 api를 호출하였다.
data를 불러올 때는, Promise API를 활용하는 HTTP 비동기 통신 라이브러리인 axios
를 사용하였다.
// api.ts
import axios from 'axios'
const api = axios.create({
baseURL: 'https://api.themoviedb.org/3/',
})
export const getMovies = () =>
api.get(`/movie/popular?api_key=${API_KEY}`).then((res) => res.data)
export const updateMovies = (id: number) =>
api
.get(`/movie/popular?api_key=${API_KEY}&page=${id}`)
.then((res) => res.data)
// Home.tsx
import { useQuery } from 'react-query'
import { getMovies } from '../utils/api'
interface IGetMoviesProps {
results: IMovieProps[]
}
interface IMovieProps {
id: number
original_title: string
}
export default function Home() {
const { data, isLoading, isError } = useQuery<IGetMoviesProps>(
'movies',
getMovies
)
if (isLoading) {
return <h4>Loading</h4>
}
if (isError) {
return <h4>Something went wrong !!</h4>
}
return (
<>
{data?.results.map((movie) => (
<div key={movie.id}>
<h4>{movie.original_title}</h4>
</div>
))}
</>
)
}
'movies'
와 같이 query key
를 사용하는 이유는 caching을 사용하기 위해서이다.
caching은 데이터가 한 번 fetch가 되면 다시 fetch를 하지 않겠다는 것을 의미한다.
예를 들어, About 페이지로 넘어갈 때, About 컴포넌트에서도 동일하게 'movies'
동일한 쿼리를 사용했다면 react query는 fetch를 또 하지 않을 것이다. 왜냐하면 'movies'
라는 key를 가진 쿼리가 이미 cache에 있기 때문이다.
import { useQuery } from 'react-query'
import { updateMovies } from '../utils/api'
export default function About() {
const { data, isLoading } = useQuery<IGetMoviesProps>('about', () =>
updateMovies(Math.floor(Math.random() * 10))
)
if (isLoading) {
return <h4>Loading</h4>
}
return (
<>
{data?.results.map(({ id, original_title }) => {
return (
<div key={id}>
<h4>{original_title}</h4>
</div>
)
})}
</>
)
}
updateMovies 함수를 호출하여 데이터를 가져올 때, page 단위로 가져오게끔 설정을 해놓았다.
Math.floor(Math.random() * 10)) , 즉 0 ~ 10 사이의 숫자를 랜덤하게 가져와 page의 데이터를 나타나게 하였는 데, 만약 숫자가 0이 나올 경우에 page=0 일때의 데이터가 없기 때문에 error가 나올 것이다.
그러나, query 에서는 기본적으로 retry의 default 값이 3
이기 때문에, 에러가 발생하여도 3번의 api 호출을 한다. 따라서 다음의 랜덤 값이 0이 아닐 경우 정상적으로 data를 불러올 수 있다.
개발자 도구
를 열어서 , Network
탭을 확인해보면
page=0
일 때는 data가 없기 때문에 실패
가 된 것을 확인할 수 있고, 따로 한번 더 요청을 하지 않아도 자동적으로 api가 요청이 되어 page=7
일때의 data가 성공
적으로 받아와지는 것을 확인 해볼 수 있다.
예를 들어, fetcher1, fetcher2, fetcher3 3개의 axios 함수를 불러온다고 가정을 해보자. 그렇다면 data
와 isLoading
의 변수명이 동일하게 3개가 발생하게 되어 에러가 발생한다.
이런 상황에서는 변수명을 각각 custom 하여 사용할 수 있다.
import { useQuery } from 'react-query'
export default function About() {
// 3개의 api를 동시에 받는 상황
const { data: oneData, isLoading: oneLoading } = useQuery('about1', fetcher1)
const { data: twoData, isLoading: twoLoading } = useQuery('about2', fetcher2)
const { data: threeData, isLoading: threeLoading } = useQuery('about3', fetcher3)
// oneLoading이 로딩중이거나, twoLoading이 로딩중이거나, threeLoading이 로딩중이거나
const loading = oneLoading || twoLoading || threeLoading
if (loading) {
return <h4>Loading</h4>
}
return (
<>
{oneData?.results.map(({ id, original_title }) => {
return (
<div key={id}>
<h4>{original_title}</h4>
</div>
)
})}
</>
)
}
해당 query를 refetch하는 함수를 사용해보자.
우선 options에 enabled: false
으로 설정해두어 component가 mount
하는 순간 query가 작동하지 않게
설정을 해둘 것이다.
button 을 클릭하면 refetch 함수가 작동하여 해당 query를 다시 작동하게 해준다.
import { useQuery } from 'react-query'
import { getMovies } from '../utils/api'
export default function Home() {
const { data, isLoading, isError, refetch } = useQuery<IGetMoviesProps>(
'home',
getMovies,
{
enabled: false,
}
)
if (isLoading) {
return <h4>Loading</h4>
}
if (isError) {
return <h4>Something went wrong !!</h4>
}
const handleRefetch = () => {
refetch()
}
return (
<>
<button onClick={handleRefetch}>refetching !</button>
{data?.results.map((movie) => (
<div key={movie.id}>
<h4>{movie.original_title}</h4>
</div>
))}
</>
)
}
버튼 클릭 시, refetch가 되어 data가 정상적으로 불러와지는 것을 확인할 수 있다.
그렇다면 여러 쿼리들을 동시에 refetch 하고 싶다면 어떻게 할 수 있을까?
코드를 효율적으로 작성하기 위하여, QueryClient
를 이용하여 cache에 접근해보자.
위에서 사용하였던 예제를 다시 사용해보자,
import { useQuery, useQueryClient } from 'react-query'
export default function About() {
const queryClient = useQueryClient();
// query key를 array 형태로 사용하였다.
const { data: oneData, isLoading: oneLoading } = useQuery(['about', 'about1'], fetcher1)
const { data: twoData, isLoading: twoLoading } = useQuery(['about', 'about2'], fetcher2)
const { data: threeData, isLoading: threeLoading } = useQuery(['about', 'about3'], fetcher3)
// about key를 가진 쿼리들을 refetch 한다.
const onRefresh = async () => {
queryClient.refetchQueries(['about'])
}
return (
<>
...
</>
)
}