최근 Redux 코드 리팩토링 과정에서 전역으로 관리되는 상태에 대한 찝찝함과 Redux 라는 보일러가 무거운 라이브러리에 대한 의구심이 들었습니다.
또한 컴포넌트별로 파편화되어 있는 로딩과 에러 상태에 대해서도 효율적으로 풀어보고 싶었습니다. 오늘 알아볼 React Query는 제가 겪고 있는 문제를 풀어줄 것이라고 판단했고 공부하고 기록하려고 합니다.
상당히 추상적인 단어인 상태는 프론트엔드 개발에서는 주어진 시간에 따라 언제든지 변경될 수 있고 원시 혹은 객체형태로 응용 프로그램에 저장되어있는 데이터를 의미합니다. 개발자의 입장에서는 이를 오너십을 가지고 관리해야할 데이터라할 수 있습니다. 이러한 상태의 변화를 관리하기란 프로덕트가 커짐에 따라 커집니다. 또한 흔히 사용되는 React는 단방향 바인딩을 원칙으로 하여 props drilling의 이슈도 피해갈 수 없습니다. 이를 해소하기 위해 Redux
Mobx
Recoil
등 상태를 관리하는 데 특화된 라이브러리도 있습니다.
이는 클라이언트에 소유되어 있지 않으며 자체적으로 제어하지 않고 원격으로 관리되는 상태라고 할 수 있습니다. 원격으로 상태를 관리하기 위해 비동기 API에 의존하고 있죠. 또한 개별적이지 않고 공유되기 때문에 값이 수시로 변할 수 있습니다. 고로 데이터를 신경 쓰지 않을 경우 out of date가 될 가능성이 있습니다. 프론트엔드에서 out of date는 캐시로 볼 수 있겠죠.
이렇게 서버로부터 동기화하는 데이터의 경우 서버가 오너쉽을 가지고 데이터를 제어하고 프론트에서는 값의 변경을 fetching을 통해서만 감지할 수 있습니다. 또한 클라이언트가 오너쉽을 가진 데이터는 서버에 저장하는 API를 통해서만 서버가 값을 감지할 수 있죠. React Query는 클라이언트에서 서버 데이터를 통한 상태를 관리하는 데 강력한 이점이 있다고 합니다.
리액트 쿼리의 공식 문서에는 본인의 주요 컨셉에 대해 Queries
Mutations
Query Invalidation
세가지로 이야기하고 있습니다.
데이터를 fetching 하는 역할을 가지고 있고 CRUD 중 Reading에만 사용됩니다.
Query Key
import {useQuery } from 'react-query'
function App() {
const info = useQuery(['todos'],fetchTodoList);
}
위 예제코드에서 ['todos']가 쿼리 키에 해당합니다. 리액트 쿼리는 쿼리 키에 따라 쿼리 캐칭을 관리합니다. v3까지는 query key를 string & array 형태로 사용해왔으나 최근 v4 업데이트를 통해 배열로만 쿼리키를 생성하도록 업데이트 되었습니다.
Query Function
위 예제코드에서 fetchtodoList가 쿼리 펑션에 해당합니다. 실질적으로 쿼리키에 따라 실행할 promise 함수를 의미합니다. 리액트 쿼리에서는 해당 함수를 쿼리 키를 통해 관리합니다.
useQuery 의 return
그리하여 promise 함수는 useQuery를 통해 새로운 값을 리턴합니다.
data
: 가장 최근의 resolved 데이터
error
: 에러 시 반환되는 객체
isFetching
: 요청 진행 중일 때 (request in-flight)
status
,isLoading
,isSuccess
: Query의 상태
refetch
: 해당 query refetch하는 함수 제공
remove
: 해당 query cache에서 지우는 함수 제공
등등...
useQuery 의 Option
useQuery
의 세번째 인자를 통해 config를 커스텀할 수 있습니다.
onSuccess
,onError
,onSettled
: query fetching 성공/실패/완료 시 실행할 side Effect 정의
enabled
: 자동으로 query를 실행시킬지 말지 여부
retry
: query 동작 실패 시, 자동으로 retry 할지?
select
: 성공 시 가져온 data를 가공해서 전달
keepPreviousData
: 새롭게 fetching 시 이전 데이터 유지 여부
refetchInterval
: 주기적으로 refetch 할지 결정하는 옵션
등등...
데이터를 업데이트하는 역할을 합니다. CRUD 중 CUD에 사용됩니다.
const mutation = useMutation(newTodo => {
return axios.post('/todos',newTodo)
})
useQuery
보다 심플하고 Promise 반환 함수만으로도 동작합니다. 추가로 Query Key를 넣어주면 devtools 에서 확인할 수 있습니다.
useMutation 의 return
const {
data,
error,
isError,
isIdle,
isLoading,
isPaused,
isSuccess,
mutate,
mutateAsync,
reset,
status,
} = useMutation(mutationFn,{option...})
mutate
: mutation
을 실행하는 함수mutateAsync
: mutate
와 비슷하지만 promise를 반환reset
: 내부 상태 초기화useMutation 의 option
const {return...} = useMutation(mutationFn, {
mutationKey,
onError,
onMutate,
onSettled,
onSuccess,
retry,
retryDelay,
useErrorBoundary,
meta,
})
optimistic update ?
api 통신 시 서버 응답값에 대해 낙관적으로 예상하고 미리 UI를 적용하는 로직.
key를 통해 기존 query를 stale 취급하여 렌더링되어있는 query들이 백그라운드에서 refetch 됩니다.
// 캐시의 모든 쿼리를 stale 취급
queryClient.invalidatequeries()
// 캐시의 'todos' 키로 시작하는 모든 쿼리를 stale 취급
queryClient.invalidatequeries('todos')
리액트 쿼리는 위의 세가지 컨셉을 통해 서버데이터의 값을 쉽게 조회하고 갱신할 수 있습니다. 하지만 리액트 쿼리는 세가지 컨셉을 메모리 캐시에 적용함으로써 그 존재의 의미가 있다고 합니다.
cacheTime
: 메모리에 얼마동안 있을 지 설정합니다. 해당 시간 이후 가비지데이터가 됩니다. (default 3000ms)staleTime
: 얼마의 시간이 흐른 후에 데이터를 stale 취급할 것인 지 설정합니다. (default 0ms)refetchOnMount
/ refetchOnWindowFocus
/ refetchOnReconnect
Mount
, window focus
, reconnect
시점에 data 가 stale이라고 판단될 경우 모두 refetch 하는 함수react query 를 통해 캐시를 활용할 경우 아래와 같은 flow 를 갖게 됩니다.
staleTime
이 만료되기 전까지 data의 상태는 fresh한 데이터라고 판단 합니다.staleTime
이 0으로 설정되어 있을 경우 data는 fetching을 통해 상태가 UI 상에 반영됨과 동시에 stale 상태가 됩니다. 고로 data의 상태는 fresh한 적이 없었다고 할 수 있습니다.staleTime
이 만료된 data의 상태를 stale로 판단합니다.refetch
이벤트 혹은 mount
window
focus
등의 옵션을 통해 refetching 하여 data 의 상태를 fresh 하게 갱신할 수 있습니다.여기까지가 active (UI에 반영되어 있음)
cacheTime
이 끝나기 전까지 inactive 상태로 메모리 상에 남아 있습니다.refetchOnMount
등의 트리거를 통해 상태를 판단하여 fresh 혹은 stale 상태로 넘어가게 됩니다.cacheTime
이 만료되면 비로소 data는 가비지컬렉터에 의해 메모리 상에서 삭제됩니다.react query 에는 설정해주지 않아도 기본적으로 알아서 세팅되어 있는 값이 있습니다. 그렇기 때문에 동작 의도를 명확히 알기 위해 더 잘 알아야 하죠
staleTime
: 0
(Queries 에서 cached data는 항상 언제나 stale 로 취급됩니다.)
refetchOnMount
/ refetchOnWindowFocus
/ refetchOnReconnect
: true
각 시점에서 data가 stale이라면 항상 refetch 하게 됩니다.
cacheTime
: 60 x 5 x 1000ms (5분)
inactive가 된 상태들은 이후 5분 뒤에 가비지컬렉터를 통해 deleted 됩니다.
retry
: 3
API 실패 시 react query 는 3번의 재시도 횟수를 가집니다.
retryDelay
: exponential backoff function (재시도 간격)
앞서 알아본 react query의 기능들로 서버에서 받아온 값들을 query key 를 통해 컴포넌트를 넘나 들며 관리하고 있습니다. 마치 전역으로 상태를 관리하는 것처럼 보이기도 합니다. React Query 는 QueryClient 내부에서 react 의 Context API를 사용하여 값들을 관리하고 있습니다.
또한 web storage api & React Query로 Redux-persist처럼 storage에 값들을 저장할 수도 있습니다.