회사에서 새로운 프로젝트 진행을 하게 되었는데 기존 프로젝트에서 사용하던 Store에 대해 고민하였고 Store에서 API를 호출하는 과정에 Store를 이렇게 사용하는게 맞나 하는 의문점이 생겼습니다. 그 후 React Query에 대해서 알게 되었고 학습하고자 '우아한테크세미나 React Query' youtube와 배민 기술 블로그를 보고 정리하게 되었습니다.
Reference에 해당 유튜브 링크를 공유해 드립니다.
UI/UX의 중요성과 함께 프로덕트 규모가 커지면서 FE가 관리해야 하는 상태가 많아졌다. 개발자 입장에서 관리해야하는 데이터들을 효율적으로 관리하는 방법으로 사용된다.
모던 웹 프론트 환경에서는 여러가지 상태들이 존재하고 이들을 체계적으로 관리하기 위해 상태관리 라이브러리 (redux, mobX, Recoil) 들이 존재한다. (Props Drilling 이슈 해결)
Store는 전역 상태가 저장되고 관리되는 공간인데 반해 실제로는 상태 관리보단 API 통신을 위한 코드로 많이 사용되고 있다.
💡 Q. 상태 관리 영역에서 API 통신, Fetching, Error 처리, 비슷한 구조의 APi 통신이 반복되고 있는데 이렇게 사용하는게 맞나!?일반적으로 서버에서 받아야하는 상태들의 특성은 다음과 같다.
ex) 음식 주문을 한 후 서버에서 관리되고 사용자 모르게 바뀌는 데이터값들이 있다.
⇒ 사실상 FE에서는 이 값들이 저장된 state들은 일종의 캐시가 된다.
기존 상태관리 라이브러리는(Redux, MobX) Server State를 관리하기 적합하지 않다...
⇒ React Query
Fetch, cache and update data in your React and React Native applications all without touching any "global state".
⇒ fetching, caching, synchronizing and updating server state in React
추가로 TS, GraphQL, SSR, Next.js, devtools 등 다양한 것들을 지원한다.
// example
import { QueryClient, QueryClientProvider, useQuery } from 'react-query'
const queryClient = new QueryClient()
export default function App() {
return (
<**QueryClientProvider** client={queryClient}>
<Example />
<**QueryClientProvider**>
)
}
function Example() {
const { **isLoading, error, data** } = **useQuery**('repoData', () =>
fetch('https://api.github.com/repos/tannerlinsley/react-query').then(res =>
res.json()
)
)
if (**isLoading**) return 'Loading...'
if (**error**) return 'An error has occurred: ' + error.message
return (
<div>
<h1>{**data**.name}</h1>
<p>{**data**.description}</p>
<strong>👀 {**data**.subscribers_count}</strong>{' '}
<strong>✨ {**data**.stargazers_count}</strong>{' '}
<strong>🍴 {**data**.forks_count}</strong>
</div>
)
}
QueryClientProvider를 기본적으로 사용해야 된다.
Key, Value 맵핑 구조이며, String 형태와 Array 형태가 있다. Query Key에 따라서 query caching을 관리한다.
// string
// A list of todos
useQuery('todos', ...) // queryKey === ['todos']
// Something else, whatever!
useQuery('somethingSpecial', ...) // queryKey === ['somethingSpecial']
//Array
// An individual todo
useQuery(['todo', 5], ...)
// queryKey === ['todo', 5]
// An individual todo in a "preview" format
useQuery(['todo', 5, { preview: true }], ...)
// queryKey === ['todo', 5, { preview: true }]
// A list of todos that are "done"
useQuery(['todos', { type: 'done' }], ...)
// queryKey === ['todos', { type: 'done' }]
Promise를 반환하는 함수이며 데이터를 resolve하거나 error를 thow할 때 사용된다.
function Todos({ status, page }) {
const result = useQuery(['todos', { status, page }], fetchTodoList)
}
// Access the key, status and page variables in your query function!
function fetchTodoList({ queryKey }) {
const [_key, { status, page }] = queryKey
return new Promise()
}
useQuery의 반환값
data, error, isFetching, status, isLoading, isSuccess, refetch, remove...
(다양하고 유용한 인터페이스를 제공해준다.)
onSuccess, onError, onSettled, enabled(자동 실행 여부), retry(동작 실패시 retry 여부), select (data 가공), keepPreviousData(새롭게 fetching 시 이전 데이터 유지 여부), refetchInterval (주기적으로 refetch 여부) .. 등 다양한 Option 설정이 가능하다.
💡 Queries 파일 분리를 추천 ⇒ domain별로 관리CRUD에서 CUD ⇒ 데이터 생성/수정/삭제용으로 사용
function App() {
const mutation = useMutation(newTodo => {
return axios.post('/todos', newTodo)
})
return (
<div>
{mutation.isLoading ? (
'Adding todo...'
) : (
<>
{mutation.isError ? (
<div>An error occurred: {mutation.error.message}</div>
) : null}
{mutation.isSuccess ? <div>Todo added!</div> : null}
<button
onClick={() => {
mutation.mutate({ id: new Date(), title: 'Do Laundry' })
}}
>
Create Todo
</button>
</>
)}
</div>
)
}
일반적으로 useQuery와 비슷하나 onMutate Option은 Optimistic update(API가 성공할 것이라고 보고 UI를 미리 업데이트, 실패 시 롤백)를 적용할 때 유용하게 사용된다.
Waiting for queries to become stale before they are fetched again doesn't always work, especially when you know for a fact that a query's data is out of date because of something the user has done. For that purpose, the QueryClient
has an invalidateQueries
method that lets you intelligently mark queries as stale and potentially refetch them too!
관리하는 query를 invalidate하는 방법
// Invalidate every query in the cache
queryClient.invalidateQueries()
// Invalidate every query with a key that starts with `todos`
queryClient.invalidateQueries('todos')
// 해당 key를 가진 query는 stale(오래된 데이터) 취급되고, 현재 rendering 되고 있는 query들은 백그라운드에서 refetch 된다.
⇒ 기존 컨셉은 RFC (5861) 새로운 데이터가 보여지기 전까지 지난 데이터를 보여준다.
Options...
Query 상태 흐름
*주의점
-staleTime은 default가 0이며, 만약에 stale하지 않은 fresh한 데이터면 값들이 retch가 일어나지 않는다.
-default 상태에서는 queries는 항상 stale 취급이며 refetch가 발생한다. inactive query들은 5분 후에 GC에 의해 처리되며 query 실패 시 3번까지 retry한다.
내부적으로 Context를 사용하기 때문에 Provider가 필요하다.
https://www.youtube.com/watch?v=MArE6Hy371c
https://techblog.woowahan.com/6339/