💻 Frontend
React-Query 튜토리얼
category
💻 Frontend
1. Introduce1-1. React Query의 사용 목적2. Set up2-1. Installation2-2. QueryClientProvider2-3. QueryClient예제. QueryProvider, QueryClient 생성3. Fetcing Data with useQuery3-1. useQuery예제. useQuery4. Handling Query Error예제. useQuery5. React Query Devtools5-1. Floating Mode5-2. Embedded Mode6. Query Cache예제. cache Time7. Stale Time예제. staleTime7-1. Stale Time과 cache Time의 차이점8. Refetch Defaults예제. refetchOn9. Polling예제. refetchInterval10. useQuery on click예제. refetch, enabled마무리느낀점References
해당 유튜브 영상을 보고 react-query에 대해 공부한 것을 정리합니다. 추가로 심화적인 개념은 공식문서를 보며 작성하고 해당 버전은 v3입니다.
1. Introduce
React Query는 React 애플리케이션에서 서버 state fetching, 캐싱, 동기화 및 업데이트를 쉽게 만드는 라이브러리 입니다.
1-1. React Query의 사용 목적
기본적으로 React 애플리에키션은 컴포넌트에서 데이터를 가져오거나 업데이트하는 방법을 제공하지 않으므로 개발자는 결국 데이터를 가져오는 고유한 방법을 구축하게 됩니다. 이것은 일반적으로 React Hooks를 사용하여 컴포넌트 상태를 결합하거나 보다 범용적인 상태관리 라이브러리를 사용하여 앱 전체에서 비동기 데이터를 제공하는 것을 의미합니다.
대부분의 상태관리 라이브러리는 client state 작업에 적합하지만 비동기 또는 server state작업에는 적합하지 않습니다. server state가 완전히 다르기 때문입니다. 우선, server state:
- 귀하가 통제하거나 소유하지 않는 위치에 원격으로 유지됩니다.
- 데이터 fetching 및 업데이트를 위한 비동기 API가 필요합니다.
- 공유 소유권을 암시하며 사용자 모르게 다른 사람이 변경할 수 있습니다.
- 주의하지 않으면 응용 프로그램에서 잠재적으로 데이터가 “구식” 될 수 있습니다.
client에서 serer state를 받고 난 후에는 더 많은 문제가 발생합니다. 예를들면 다음과 같습니다:
- 캐싱… (아마 프로그래밍에서 가장 어려운 일)
- 동일한 데이터에 대한 여러 요청을 단일 요청으로 중복 제거
- 데이터가 “오래된”시점을 알고, 백그라운드에서 “오래된”데이터 업데이트
- 데이터 업데이트를 최대한 신속하게 반영
- pagination 및 lazy loading 데이터와 같은 성능 최적화
- 서버 상태의 메모리 관리 및 가비지 수집
대부분의 사람들은 이러한 문제를 아직 해결하지 못했고 단지 표면만 잘 보이게 해결하였을 것입니다.
React Query는 server state를 관리하기 위한 최고의 라이브러리입니다. 기본 구성 없이 놀라울 정도로 잘 작동하며, 애플리케이션이 커짐에 따라 원하는 대로 커스터마이징할 수 있습니다. React Query를 사용하면 서버 state의 까다로운 문제와 장애물을 물리치고 극복라며 앱 데이터가 사용자를 제어하기 전에 제어할 수 있습니다.
보다 기술적인 측면에서 React Query의 장점은 다음과 같습니다.
- 애플리케이션에서 복잡하고 많은 코드 줄을 제거하고 몇 줄의 React Query 로직으로 대체할 수 있습니다.
- 새 server state 데이터 소스 연결에 대한 걱정없이 애플리케이션을 유지 관리하기 쉽고 새 기능을 쉽게 구축할 수 있습니다.
- 애플리케이션이 어느때보다 빠르고 응답성이 높게하여 사용자에게 직접적인 영향을 미칠 수 있습니다.
- 잠재적으로 대역폭을 절약하고 메모리 성능을 높일 수 있습니다.
2. Set up
2-1. Installation
$ npm i react-query # or $ yarn add react-query
2-2. QueryClientProvider
QueryClientProvider 컴포넌트를 사용하여 애플리케이션에 QueryClient를 연결하고 제공합니다.
options
- client: QueryClient
필수 속성, QueryClient를 제공할 인스턴스
- contextSharing: boolean
기본값은 false, context 공유를 활성화하려면 이 설정을 true로 설정합니다.
2-3. QueryClient
QueryClient는 캐시와 상호작용하는데 사용할 수 있습니다.
import { QueryClient } from 'react-query' const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: Infinity, }, }, }) await queryClient.prefetchQuery('posts', fetchPosts)
QueryClient는 옵션이 너무 많아서.. 다음에 제대로 정리할 것 입니다.
예제. QueryProvider, QueryClient 생성
// App.js import { QueryClient, QueryClientProvider } from 'react-query' const queryClient = new QueryClient() function App() { return <QueryClientProvider client={queryClient}>...</QueryClientProvider> }
3. Fetcing Data with useQuery
3-1. useQuery
const {} = useQuery( 'queryKey', queryFn, { options } )
- queryKey: string | unknown[ ]
필수속성, 해당쿼리에 사용할 쿼리 키입니다.
쿼리 키는 안정적인 해시로 해시됩니다.
이 키가 변경되면 쿼리가 자동으로 업데이트 됩니다.(enabled가 false로 설정되지 않은 경우)
- queryFn: (context: QueryFunctionContext) => Promise<TData>
필수 속성, 하지만 기본 쿼리 함수가 정의되지 않은 경우에만 해당합니다.
쿼리가 데이터를 요청하는 데 사용하는 함수입니다.
응답 데이터 혹은 에러를 발생시키는 Promise를 반환해야 합니다.
예제. useQuery
- Not Use react-query
// SuperHeroes.page.js import { useState, useEffect } from "react"; import axios from "axios"; export const SuperHeroesPage = () => { const [isLoading, setIsLoading] = useState(true); const [data, setData] = useState([]); useEffect(() => { axios .get("http://localhost:4000/superheroes") .then((res) => { setData(res.data); setIsLoading(false); }) }, []); if (isLoading) { return <h2>Loading...</h2>; } return ( <> <h2>Super Heroes Page</h2> {data.map((hero) => { return <div>{hero.name}</div>; })} </> ); };
- Use react-query
// RQSuperHeroes.page.js import { useQuery } from "react-query"; import axios from "axios"; const fetchSuperHeroes = () => { return axios.get("http://localhost:4000/superheroes"); }; export const RQSuperHeroesPage = () => { const { isLoading, data } = useQuery( "super-heroes", fetchSuperHeroes, ); if (isLoading) { return <h2>Loading...</h2>; } return ( <> <h2>React Query Super Heroes Page</h2> {data.data.map((hero) => { return <div key={hero.name}>{hero.name}</div>; })} </> ); };
- isLoading: boolean
데이터를 로딩중인지 판단하는 변수입니다.
로딩중이면 true, 아니면 false 입니다.
- data: TData
기본값은 undefined, 쿼리에 대해 성공적으로 응답받은 최신 데이터입니다.
4. Handling Query Error
예제. useQuery
- Not Use react-query
import { useState, useEffect } from "react"; import axios from "axios"; export const SuperHeroesPage = () => { const [isLoading, setIsLoading] = useState(true); // ✨new const [error, setError] = useState(""); const [data, setData] = useState([]); useEffect(() => { axios .get("http://localhost:4000/superheroes") .then((res) => { setData(res.data); setIsLoading(false); }) // ✨new .catch((error) => { setError(error.message); setIsLoading(false); }); }, []); if (isLoading) { return <h2>Loading...</h2>; } // ✨new if (error) { return <h2>{error}</h2>; } return ( <> <h2>Super Heroes Page</h2> {data.map((hero) => { return <div>{hero.name}</div>; })} </> ); };
- Use react-query
import { useQuery } from "react-query"; import axios from "axios"; const fetchSuperHeroes = () => { return axios.get("http://localhost:4000/superheroes"); }; export const RQSuperHeroesPage = () => { // ✨new const { isLoading, data, isError, error } = useQuery( "super-heroes", fetchSuperHeroes, ); if (isLoading) { return <h2>Loading...</h2>; } // ✨new if (isError) { return <h2>{error.message}</h2>; } return ( <> <h2>React Query Super Heroes Page</h2> {data.data.map((hero) => { return <div key={hero.name}>{hero.name}</div>; })} </> ); };
- isError: boolean
오류가 발생한지 상태를 판단하는 변수입니다.
에러가 발생하면 true, 아니면 false 입니다.
- error: null | TError
기본값은 null, 오류가 발생한 경우 쿼리에 대한 오류 객체입니다.
5. React Query Devtools
React Query를 공부할 때 개발도구가 있기를 원할 겁니다. React Query Devtools는 내부 작업을 시각화하는 데 도움이 되며 디버깅 시간을 절약할 수 있습니다!
devtools는 react-query/devtools 패키지로 추가 설치없이 다음과 같이 불러옵니다:
import { ReactQueryDevtools } from 'react-query/devtools'
기본적으로 React Query Devtools는 개발환경에서만 포함되므로, 제품에 올라가는 것에 대해 걱정할 필요가 없습니다.
- fresh : 새롭게 추가된 쿼리 & 만료되지 않은 쿼리 → 컴포넌트가 마운트, 업데이트되어도 데이터 재요청 X
- fetching: 요청 중인 쿼리
- stale: 만료된 쿼리 → 컴포넌트가 마운트, 업데이트 되면 데이터 재요청
- inactive: 비활성화된 쿼리 → 캐시 시간이 지나면 가비지 컬렉터에 위해 제거
5-1. Floating Mode
플로팅 모드는 개발자 도구를 앱에 고정된 플로팅 요소로 제공합니다. 다음 코드를 React 애플리에키션의 가능한 최상위에 배치하세요. 페이지의 root에 가까울수록 더 잘 작동합니다!
import { QueryClient, QueryClientProvider } from "react-query"; // ✨new import { ReactQueryDevtools } from "react-query/devtools"; const queryClient = new QueryClient(); function App() { return ( <QueryClientProvider client={queryClient}> // ✨new <ReactQueryDevtools initialIsOpen={false} position="bottom-right" /> </QueryClientProvider> ); } export default App;
options
- initalIsOpen: Boolean
개발 도구가 기본적으로 열려 있도록 하려면 true로 설정
- panelProps: PropsObject
이 속성을 사용하여 패널에 props을 추가합니다. 예를 들어 className, style 등을 추가할 수 있습니다.
- closeButtonProps: PropsObject
이 속성을 사용하여 닫기 버튼에 props을 추가합니다. 예를 들어 className, style, onClick 등을 추가할 수 있습니다.
- toggleButtonProps: PropsObject
이 속성을 사용하여 토글 버튼에 props를 추가합니다.
- position?: "top-left" | "top-right" | "bottom-left" | "bottom-right"
기본값은 “bottom-left”, devtools 버튼의 위치를 설정합니다.
5-2. Embedded Mode
임베디드 모드는 devtools를 애플리케이션의 일반 구성요소로 임베드합니다. 원하는대로 스타일링을 지정할 수 있습니다.
import { ReactQueryDevtoolsPanel } from 'react-query/devtools' function App() { return ( <QueryClientProvider client={queryClient}> {/* The rest of your application */} <ReactQueryDevtoolsPanel style={styles} className={className} /> </QueryClientProvider> ) }
- style: StyleObject
인라인 스타일로 컴포넌트의 스타일을 지정하는 데 사용되는 표준 React 스타일 객체
- className: string
class로 컴포넌트의 스타일을 지정하는 데 사용되는 표준 React className 속성
6. Query Cache
react-query를 통해 쿼리를 요청하면 기본적으로 캐싱기능을 지원해줍니다. 그렇기 때문에 특정 시간 내에 똑같은 요청을 보내면 캐싱된 응답 값을 보내주어 같은 요청에 대한 리소스를 절약시켜줍니다. 자주 변경되지 않는 데이터에 대한 네트워크 요청 수를 줄이는 것이죠.
예제. cache Time
import { useQuery } from "react-query"; import axios from "axios"; const fetchSuperHeroes = () => { return axios.get("http://localhost:4000/superheroes"); }; export const RQSuperHeroesPage = () => { // ✨new const { isLoading, data, isError, error, isFetching } = useQuery( "super-heroes", fetchSuperHeroes, // ✨new { cacheTime: 5000, } ); console.log({ isLoading, isFetching }); if (isLoading) { return <h2>Loading...</h2>; } if (isError) { return <h2>{error.message}</h2>; } return ( <> <h2>React Query Super Heroes Page</h2> {data.data.map((hero) => { return <div key={hero.name}>{hero.name}</div>; })} </> ); };
위 예제를 확인해보면 isLoading은 처음 로딩 이후에는 계속 false여서 로딩창을 한번만 볼 수 있습니다.
- cacheTime: number | Infinity
기본값은 5분으로 기본적으로 5분간 데이터를 캐싱한다.
unused/inactive 캐시 데티터가 메모리에 남아있는 시간입니다. 쿼리 캐시가 사용되지 않거나 비활성화되면 해당 캐시 데이터는 가비지에 수집됩니다.
Infinity로 설정하면 가비지 컬렉션이 비활성화됩니다.
- isFetching: boolean
백그라운드에서 refetch 하는 것 뿐만 아니라 초기 로드를 포함하여
요청이 진행 중일 때마다 true입니다.
예제처럼 옵션에 cacheTime 5000을 주면 5초동안 데이터를 캐싱하고 5초가 지나면 버려집니다.
7. Stale Time
stale이란 사전적 의미로는 “신선하지 않는”이라는 뜻을 가진다. 신선하지 않다는 것은 서버 요청을 받은 이후에 데이터가 변경이 되어 낡고 오래된 데이터를 stale하다는 뜻이다. refetch가 되지않는 조건은 쿼리가 fresh상태일 때 만료되지 않은 쿼리라고 판단한다. stale 상태에서는 refetch 시켜주기 때문에 그 시간을 stale time으로 조절하여 refetch 시간을 조절할 수 있다.
예제. staleTime
export const RQSuperHeroesPage = () => { const { isLoading, data, isError, error, isFetching } = useQuery( "super-heroes", fetchSuperHeroes, { // ✨new staleTime: 30000, } ); };
위 예제를 확인해보면 staleTime은 3초이기때문에 3초 후에 fresh 상태에서 stale 상태로 변하게 된다. 3초동안 fresh 상태이기때문에 계속 요청을 보내도 캐싱된 데이터를 불러온다. 하지만 3초후에는 stale 상태로 변하기 때문에 그 사이 server state가 변하게 되면 변경된 데이터를 받을 수 있다.
- staleTime: number | Infinity
선택 사항이고 기본 값은 0이다.
데이터가 fresh → stale 상태로 변경되는데 걸리는 시간이다.
fresh 상태는 쿼리 인스턴스가 새롭게 mount되어도 refetch가 일어나지 않는다.
데이터가 한번 fetch 되고 나서 staleTime이 지나지 않았다면 unmount 후 mount 되어도 fetch가 일어나지 않는다.
Infinity로 설정하면 데이터가 오래되었다고 판단하지 않습니다. → refetch하지 않는다, 항상 fresh인 상태
7-1. Stale Time과 cache Time의 차이점
- Stale Time
데이터가 fresh 상태에서 stale 상태로 변경되는데 걸리는 시간을 말한다.
fresh 상태일때는 페이지를 이동했다가 돌아왔을 경우에도 fetch가 일어나지 않는다.
즉, 데이터가 한번 fetch 되고 staleTIme이 지나지 않았다면 unmount 후 mount가 발생해도 다시 fetch가 발생하지 않는다.
default 값이 0이기 때문에 받아오는 즉지 stale하다고 판단해서 캐싱 데이터와는 무관하게 계속 fetching을 수행한다. 그러므로 staleTime을 지정하지 않고 사용한다면 React Query의 캐싱 기능을 활용할 수 없다.
자주 변경되는 데이터라면 지정하지 않는 편이 좋지만, 정적인 데이터 또는 자주 변경될 필요가 없는 데이터라면 staleTime을 지정해서 서버의 부담을 줄여주는 것이 좋다.
- cacheTime
데이터가 inactive 상태일 때 캐싱된 상태로 남아있는 시간을 말한다.
쿼리 인스턴스가 unmount되면 데이터는 inactive 상태로 변경되며, 캐시는 cacheTime 만큼 유지된다. 이걸 cacheTime 만큼 유지시키는 이유는 쿼리 인스턴스가 다시 마운트되면 데이터를 fatch하는 동안 cacheTime이 지나지 않은 캐시 데이터를 보여준다.
cacheTime이 지나면 가비지 콜렉터로 삭제가 된다.
유의할 점은 staleTime이 cacheTime보다 길더라도 cacheTime이 지나면 데이터가 사라지기 때문에 적용할 때 staleTime보다 cacheTime이 더 길어야 한다.
8. Refetch Defaults
예제. refetchOn
export const RQSuperHeroesPage = () => { const { isLoading, data, isError, error, isFetching } = useQuery( "super-heroes", fetchSuperHeroes, { // ✨new refetchOnMount: true, refetchOnWindowFocus: true, } ); };
- refetchOnMount: boolean | "always" | ((query: Query) => boolean | "always")
선택사항이고 기본 값은 true이다.
true로 설정하면 데이터가 stale 경우 쿼리가 마운트 시 다시 가져온다.
false로 설정하면 마운트 시 쿼리를 다시 가져오지 않습니다. ( 이전 데이터를 가져옴 )
always로 설정하면 마운트 시 쿼리는 항상 다시 가져온다.
함수로 설정하면 쿼리와 함께 함수가 실행되어 값을 계산한다.
- refetchOnWindowFocus: boolean | "always" | ((query: Query) => boolean | "always")
선택사항이고 기본 값은 true이다.
true로 설정하면 쿼리가 stale 경우 창을 포커스 할 때 데이터를 다시 가져온다.
false로 설정하면 쿼리를 창을 포커스 할 때 데이터를 다시 가져오지 않는다.
always로 설정하면 쿼리는 항상 창을 포커스할 때 다시 가져온다.
함수로 설정하면 쿼리와 함께 함수가 실행되어 값을 계산한다.
위에 예제를 보면 마운트 될때마다 쿼리를 요청하고 창을 왔다 갔다 할 때마다 쿼리를 요청합니다.
9. Polling
폴링(Polling)은 웹 애플리케이션이나 시스템에서 주기적으로 데이터를 가져오는 방식을 말합니다.
즉, 일정한 간격으로 서버에 데이터를 요청하여 최신 정보를 가져옵니다.
예제. refetchInterval
export const RQSuperHeroesPage = () => { const { isLoading, data, isError, error, isFetching } = useQuery( "super-heroes", fetchSuperHeroes, { // ✨new refetchInterval: 2000, refetchIntervalInBackground: true, } ); };
- refetchInterval: number | false | ((data: TData | undefined, query: Query) => number | false)
선택사항이고, 숫자로 설정하면 모든 쿼리가 ms단위로 지속적으로 다시 가져온다.
함수로 설정하면 최신 데이터와 쿼리로 함수를 실행하여 빈도를 계산한다.
- refetchIntervalInBackground: boolean
선택사항이고, true로 설정하면 refetchInterval을 사용하여 지속적으로 다시 가져오도록 설정된 쿼리는 탭/창이 백그라운드에 있는 동안 계속 다시 가져옵니다.
위의 예제를 보면 2초마다 쿼리를 요청하고 창을 보고 있지 않아도 지속적으로 2초마다 요청합니다.
10. useQuery on click
예제. refetch, enabled
export const RQSuperHeroesPage = () => { const { isLoading, data, isError, error, isFetching, refetch } = useQuery( "super-heroes", fetchSuperHeroes, { // ✨new enabled: false, } ); return ( <> <h2>React Query Super Heroes Page</h2> // ✨new <button onClick={refetch}>Fetch heroes</button> {data?.data.map((hero) => { return <div key={hero.name}>{hero.name}</div>; })} </> ); };
- enabled: boolean
쿼리가 자동으로 실행되지 않도록 하려면 이것을 false로 설정한다.
- refetch: (options: { throwOnError: boolean, cancelRefetch: boolean }) => Promise<UseQueryResult>
쿼리를 수동으로 다시 가져오는 기능이다.
쿼리 오류인 경우 오류만 기록된다. 오류를 발생시키려면 throwOnError: true 옵션을 지정해주면 된다.
위의 예제를 보면 useQuery는 useEffect처럼 첫 렌더링 시에 호출을 하는데 enabled를 false로 설정하면 첫 렌더링 시 쿼리가 자동으로 실행되지 않습니다. 하지만 refetch를 불러 button 클릭 시 실행시키게 하면 쿼리를 요청합니다.
마무리
느낀점
처음 공부할 때 막막함이 먼저였지만, 공식문서를 보고 잘 짜여진 예제 영상을 보니 쉽게 이해할 수 있었다.
server state를 요청하고 관리하는 것에 불편함이 많은데, react-query를 사용하면 그 부분을 편하게 관리 할 수 있다는 점이 react-query가 인기 있는 이유이지 않을까라고 생각하게 되었다. data fetching, error handling, refetch 등등.. 기존 코드로 작성하면 번거로운 부분들을 react-query로 쉽고 추상적인 코드를 작성할 수 있다는 장점을 알게되었다.
새로운 프로젝트에서 react-query를 도입하여 server state관리 이외에도 Infinity Scroll, Pagination도 구현해보고 싶다!