React Query
는 리액트 애플리케이션에서 global state
없이 서버 데이터 가져오기, 캐싱, 동기화 및 업데이트를 쉽게 만들어 주는 서버 상태 관리 라이브러리다.
React Query의 등장
React
는 서버에서 데이터를 가져오거나 업데이트하는 명확한 방법을 제공하지 않는다. 따라서 개발자들은 데이터를 가져오기 위한 처리를 따로 해주어야 했다.
일반적으로 컴포넌트에서 hook을 사용하여 데이터를 가져와 상태를 관리하거나, 전역 상태 관리 라이브러리를 사용하여 store
에 비동기 데이터를 저장하고 가져왔다.
이때 store
는 전역 상태를 관리하는 저장소로 theme
나 locale
과 같은 client state
관리에는 적합하지만, 서버에서 가져오는 데이터 server state
관리에는 적합하지 않다. 그 이유는 다음과 같다.
Server State는 다음과 같은 특징을 가진다.
client
에서 제어하거나 소유하지 않는 원격의 공간에서 관리되고 유지된다.Fetching
이나Updating
을 위한 비동기 API가 필요하다.- 다른 사용자와 공유되고 있으며 사용자가 모르게 다른 사람이 변경할 수 있다.
- 신경쓰지 않을 경우 애플리케이션이 잠재적으로 오래된 상태(
out of date
)가 될 수 있다.
Client State와의 비교
- Client에서 소유하며 온전한 제어가 가능하다.
- 초기값 설정이나 변경에 제약이 없다.
- 다른 사람들과 공유되지 않는다.
- Client 내에서 UI/UX 흐름이나 사용자 인터렉션에 따라 변할 수 있다.
- 항상 Client 내에서 최신 상태로 관리된다.
따라서 해당 state
값을 store
에서 관리하게 된다면 문제가 발생할 수 있다. 스토어에서 관리하기 어렵거나 애매한 값에 대해 예를 들면 다음과 같다.
- 캐싱
- 반복되는 isFetching, isError 등 API 관련 상태값과 비슷한 구조의 통신 코드
- 동일한 데이터에 대한 여러 요청을 단일 요청으로 중복 제거
- 백그라운드에서
out of date
데이터 업데이트 - 최대한 빠르게 업데이트된 데이터 반영
- 페이지 처리나 지연 로딩 데이터와 같은 성능 최적화
- 구조적 공유를 통한 쿼리 결과 메모이제이션
위와 같은 문제를 해결하기 위해 React Query
를 사용할 수 있다. React Query
는 구성없이 즉시 사용이 가능하고 원한다면 config를 커스텀 할 수 있다.
React Query 설치
npm
$ npm i react-query
# or
$ yarn add react-query
CDN
<script src="https://unpkg.com/react-query/dist/react-query.production.min.js"></script>
Quick Start
다음은 React Query
를 사용하는 간단한 예제이다. 해당 예제에서는 React Query
의 3가지 핵심 개념을 간략하게 살펴볼 수 있다.
- Queries
- Mutations
- Query Invalidation
import {
useQuery,
useMutation,
useQueryClient,
QueryClient,
QueryClientProvider,
} from 'react-query'
import { getTodos, postTodo } from '../my-api'
// QueryClient를 생성한다.
const queryClient = new QueryClient()
function App() {
return (
// QueryClientProvider에 생성한 client를 넘겨준다.
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
)
}
function Todos() {
// useQueryClient hook을 통해 클라이언트에 접근할 수 있다.
const queryClient = useQueryClient()
// Queries
const query = useQuery('todos', getTodos)
// Mutations
const mutation = useMutation(postTodo, {
onSuccess: () => {
// 해당 쿼리 키 결과 쿼리를 무효화시키고 다시 호출한다.
queryClient.invalidateQueries('todos')
},
})
return (
<div>
<ul>
{query.data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
<button
onClick={() => {
mutation.mutate({
id: Date.now(),
title: 'Do Laundry',
})
}}
>
Add Todo
</button>
</div>
)
}
render(<App />, document.getElementById('root'))
React Query
를 사용하기 위해서는 QueryClientProvider
로 감싸주어야 한다. 이때 넘기는 client
의 값은 QueryClient
의 인스턴스이다. QueryClient
의 자세한 정보는 공식문서를 살펴보길 바란다.
Default Config
React Query의 lifecycle
데이터 상태주기 : isFetching -> fresh -> stale -> inActive -> GC
- A 쿼리 인스턴스 mount
- 데이터 fetch 후 쿼리 캐시에 A라는 키로 저장
- 해당 데이터는
fresh
상태에서staleTime
이후state
상태로 변경 - 이때
staleTime
은 이 데이터가 최신 데이터임을 증명해주는 시간
- 해당 데이터는
- A 쿼리 인스턴스 unmount
- 쿼리 캐시로 저장된 값은
cacheTime
만큼 유지되다가 가비지 콜렉터로 수집된다.- 이때
cacheTime
이 지나기 전, A 쿼리 인스턴스가 다시 mount되면 fetch가 실행되어fresh
한 값을 가져오기 전까지 캐시 데이터를 보여준다.
- 이때
StaleTime과 CacheTime
- staleTime
- 데이터가
fresh
에서stale
상태로 변경되는데 걸리는 시간 - 해당 시간이 지난 데이터는 오래된 데이터로 간주된다.
- 해당 시간동안에는 쿼리 인스턴스가 새롭게 mount 되어도 데이터 fetch가 일어나지 않는다.
- 데이터가
- cacheTime
- 쿼리 인스턴스가 unmount 되면서 데이터가
inactive
상태로 변경되었을 때 캐싱 결과로 남아 있는 시간 cacheTime
은staleTime
과 관계없이 무조건inactive
된 시점을 기준으로 삭제를 결정한다.
- 쿼리 인스턴스가 unmount 되면서 데이터가
- default
- staleTime:
0
- cacheTime
5 min
- staleTime:
refetch 수행
- 아래의 경우 발생 시 자동으로
refetch
수행 - default:
true
- refetchOnMount : 마운트됨
- refetchOnWindowFocus : 창에 다시 초점 맞춤
- refetchOnReconnect : 네트워크 재연결
retry 수행
- 실패한 쿼리는 자동으로 3번 재시도, delay는 1000초에서 실패한 횟수와 비례해서 증가
- default
- retry:
3
- retryDelay:
attempt => Math.min(attempt > 1 ? 2 ** attempt * 1000 : 1000, 30 * 1000)
- retry:
QueryKey
array
또는string
- 일관성을 유지하기 위해 항상 배열 사용을 권장
- react-query는 내부적으로 Array로 변환함
// 🚨 결과적으로 ['todos'] 로 변환됨
useQuery("todos");
// ✅
useQuery(["todos"]);
쿼리키 구조
- 일반적인 쿼리 키에서 구체적인 쿼리 키로 구조화하기
['todos', 'list', { filters: 'all' }]
['todos', 'list', { filters: 'done' }]
['todos', 'detail', 1]
['todos', 'detail', 2]
- 해당 구조 사용시 모든 목록이나 세부 정보
['todos']
를 무효화 시킬 수 있을 뿐만 아니라 특정 대상['todos', 'detail', 1]
만 지정할 수 있음 -> Muation Response의 업데이트를 유연하게 처리 가능
쿼리키 비교
- 쿼리키는 해시값으로 관리
- 객체의 순서에 관계 없이 모두 동일 해시값
- 배열의 항목 순서가 바뀌면 다른 해시값
// 모두 동일
useQuery(['todos', { status, page }], ...)
useQuery(['todos', { page, status }], ...)
useQuery(['todos', { page, status, other: undefined }], ...)
// 모두 상이
useQuery(['todos', status, page], ...)
useQuery(['todos', page, status], ...)
useQuery(['todos', undefined, page, status], ...)
쿼리 키 팩토리
- 쿼리 키를 수동적으로 선언
- 쿼리 키 팩토리 사용해서 관리 (추천)
const todosKeys = {
all: ["todos"] as const,
lists: () => [...todosKeys.all, "list"] as const,
list: (filters: object | string) =>
[...todosKeys.lists(), { filters }] as const,
details: () => [...todosKeys.all, "detail"] as const,
detail: (id: number | string) => [...todosKeys.details(), id] as const,
};
// 🕺 'todos'와 관련된 모든 쿼리 제거
queryClient.removeQueries(todoKeys.all);
// 🚀 모든 리스트 관련 쿼리 무효화
queryClient.invalidateQueries(todoKeys.lists());
// 🙌 단일 쿼리 prefetch
queryClient.prefetchQueries(todoKeys.detail(id), () => fetchTodo(id));
Query Basics
- 모든 쿼리 결과는 쿼리 키에 종속적 ->
QueryClient
를 통해 캐시된 값 가져오기 가능 isLoading
,isError
,isSuccess
상태 반환 (status
로 받는 것도 가능)isLoading
:status === loading
isError
:status === isError
isSuccess
:status === isSuccess
error
,data
정보 반환
useQuery
일반적으로 데이터를 조회하는 경우 사용
function usePosts() {
return useQuery(["posts"], async () => {
const { data } = await axios.get("https://jsonplaceholder.typicode.com/posts");
return data;
});
}
function Posts({ setPostId }) {
const queryClient = useQueryClient();
const { status, data, error, isFetching } = usePosts();
return (
<div>
<h1>Posts</h1>
<div>
{status === "loading" ? (
"Loading..."
) : status === "error" ? (
<span>Error: {error.message}</span>
) : (
<>
<div>
{data.map((post) => (
<p key={post.id}>
<a
onClick={() => setPostId(post.id)}
href="#"
style={
// post 정보가 캐시에 존재하면 스타일 변경
queryClient.getQueryData(["post", post.id])
? {
fontWeight: "bold",
color: "green",
}
: {}
}
>
{post.title}
</a>
</p>
))}
</div>
<div>{isFetching ? "Background Updating..." : " "}</div>
</>
)}
</div>
</div>
);
}
Enabled option
enabled: false
로 설정refetch
로 가져오기 (상태disabled
로 유지됨)
enabled
에state
변수 사용enabled
조건에state
변수 사용 ->Lazy Query
상태가 disabled
로 유지되면 새 인스턴스가 마운트될 때 쿼리가 백그라운드에서 자동으로 다시 가져오지 않는다. 또한 queryClient
의 일반적으로 쿼리를 다시 가져오는 호출인 invalidateQueries
와 refetchQueries
를 무시한다.
Lazy Queries
function Todos() {
const [filter, setFilter] = React.useState('')
const { data } = useQuery(
['todos', filter],
() => fetchTodos(filter),
{
// ⬇️ filter가 비어있을 경우에만 disabled
enabled: !!filter
}
)
return (
<div>
// 🚀 filter 적용시 enable 되면서 쿼리 실행
<FiltersForm onApply={setFilter} />
{data && <TodosTable data={data}} />
</div>
)
}
useMutation
일반적으로 데이터를 생성/업데이트/삭제하는 경우 사용
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={() => {
// mutate 호출 했을 때 인자로 넘겨준 함수 실행됨
mutation.mutate({ id: new Date(), title: 'Do Laundry' });
}}
>
Create Todo
</button>
</>
)}
</div>
);
}
Mutation Side Effects
useMutation(addTodo, {
onMutate: variables => {
// mutate 호출되기 전
// 리턴 값은 context에서 사용할 값 반환
return { id: 1 }
},
onError: (error, variables, context) => {
// 에러 발생 시
console.log(`rolling back optimistic update with id ${context.id}`)
// context.id 의 값은 onMutate의 반환 값에서 가져옴
},
onSuccess: (data, variables, context) => {
// 요청 성공 시
},
onSettled: (data, error, variables, context) => {
// 요청이 성공 또는 에러 발생 시
// onError/onSuccess 호출 -> onSettled 호출
},
})
Optimistic Updates
const queryClient = useQueryClient()
useMutation(updateTodo, {
// mutate 함수 호출 시
onMutate: async newTodo => {
// 낙관적 업데이트를 덮어쓰지 않도록 쿼리 패치 취소
await queryClient.cancelQueries('todos')
// 기존 값 스냅샷
const previousTodos = queryClient.getQueryData('todos')
// 새로운 값으로 낙관적 업데이트
queryClient.setQueryData('todos', old => [...old, newTodo])
// 스냅샷 값을 담은 컨텍스트 객체를 반환
return { previousTodos }
},
// mutation 실패 시, 컨텍스트 값으로 onMutate에서 한 낙관적 업데이트 롤백
onError: (err, newTodo, context) => {
queryClient.setQueryData('todos', context.previousTodos)
},
// 성공하거나 실패 시 쿼리 refetch
onSettled: () => {
queryClient.invalidateQueries('todos')
},
})
'Frontend' 카테고리의 다른 글
Intersection Observer API를 활용한 무한스크롤 구현 (0) | 2022.08.17 |
---|---|
[Javascript] Throttle 과 Debounce (0) | 2022.05.12 |
[Redux] 간단한 예제로 살펴보는 리덕스의 동작 원리 (0) | 2022.03.17 |
[React] ContextAPI & useContext Hook을 통한 Global State 값 관리하기 (0) | 2022.03.11 |
[Javascript] 변수, 호이스팅, TDZ (0) | 2022.03.07 |