2023. 11. 16. 04:09ㆍ카테고리 없음
useQuery의 반환값들
크게 두 범주로 생각할 수 있는데, 캐쉬된 데이터의 상태와 데이터 페칭에 대한 상태로 생각하면 쉽다.
데이터에 대한 상태값
- status === ‘pending’ 쿼리에 데이터가 없을 경우 isPending
- status === 'error' 쿼리에 오류가 생긴 경우 isError
- status === 'success' 쿼리가 성공적이었고 데이터가 사용 가능한 경우 isSuccess
페칭 상태에 대한 값
- fetchStatus === 'fetching' - 쿼리가 페칭 중일때
- fetchStatus === 'paused' - 쿼리는 페치를 하고 싶지만, 일시중지 됐을때.
- fetchStatus === 'idle' - 쿼리가 어떤 것도 하지 않을때
왜 두 가지 상태가 필요할까?
- 백그라운드 리패치와 swr 로직은 두 가지 상태의 모든 조합을 고려하기 때문이다.
- isSuccess 상태의 쿼리는 일반적으로 idle 상태이지만 백그라운드 리페치가 일어나면 fetching 상태다.
- 마운트되고 데이터가 없는 쿼리는 일반적으로 isPending 상태이고 fetching상태. 하지만 네트워크 연결이 돼있지 않다면 paused 상태이다.
query keys
단일 키 외에 더 많은 정보가 필요한 경우 문자열 또는 직렬화 가능한 개체를 배열에 담아 키로서 사용 가능하다. 예시를 들어보면, 계층적 또는 중첩된 리소스일 경우 ID나 인덱스를 전달할 수 있다.
// An individual todo
useQuery({ queryKey: ['todo', 5], ... })
// An individual todo in a "preview" format
useQuery({ queryKey: ['todo', 5, { preview: true }], ...})
// A list of todos that are "done"
useQuery({ queryKey: ['todos', { type: 'done' }], ... })
중요한 점은 쿼리 키는 deterministic 하게 결정된다는 점이다.
하단의 예제의 경우 모두 같은 쿼리로 인식된다.
useQuery({ queryKey: ['todos', { status, page }], ... })
useQuery({ queryKey: ['todos', { page, status }], ...})
useQuery({ queryKey: ['todos', { page, status, other: undefined }], ... })
하단의 예제의 경우 모두 다른 쿼리로 인식된다.
useQuery({ queryKey: ['todos', status, page], ... })
useQuery({ queryKey: ['todos', page, status], ...})
useQuery({ queryKey: ['todos', undefined, page, status], ...})
쿼리 키의 경우 페칭하는 데이터를 고유하게 설명하므로 쿼리 함수에서 변경되는 모든 변수를 쿼리 키에 포함해야 한다.
즉 쿼리 키는 쿼리 함수에 대한 dependencies 역할을 한다. (useEffect의 dependency array와 유사하다.)
enabled 옵션
쿼리가 마운트시 자동으로 실행되지 않도록 하려면 enabled에 false 값을 주면 된다.
만약 enabled가 false라면...
- 마운트시 쿼리가 페치를 하지 않는다.
- 쿼리가 백그라운드 페칭을 하지 않는다.
- 쿼리에 캐시된 데이터가 있다면 status === "success" 상태로 초기화.
- 쿼리에 캐시된 데이터가 없다면 status === "pending" 그리고 fetchStatus === "idle" 상태로 초기화.
- 쿼리클라이언트의 invaldateQueries 및 refetchQueries 호출을 무시한다.
즉 useQuery에서 반환된 refetch로 수동적으로 데이터 페칭을 트리거해야만 데이터를 가져온다는건데, 이렇게 하면 과거 useEffect나 이벤트 핸들러에 데이터 페칭 함수들을 붙이는 것과 달라지는게 없다.
공식문서에서는 다음과 같이 enabled에 false를 넣어서 영구적으로 비활성화하는 것보단 다음과 같이 lazy queries를 사용하는걸 추천한다.
function Todos() {
const [filter, setFilter] = React.useState('')
const { data } = useQuery({
queryKey: ['todos', filter],
queryFn: () => fetchTodos(filter),
// ⬇️ disabled as long as the filter is empty
enabled: !!filter
})
return (
<div>
// 🚀 applying the filter will enable and execute the query
<FiltersForm onApply={setFilter} />
{data && <TodosTable data={data}} />
</div>
)
}
말만 lazy query라고 어렵게 붙여놨지만 사실 enabled에 false값 말고 프로그래밍된 값을 넣으라는 개념이다.
아무튼 lazy query의 경우 처음 statue는 pending 상태이다. (pending이 의미하는 바는 데이터가 없는 경우기 때문)
하지만 이 경우 isPending 플래그를 사용해서 로딩 스피너를 사용하는 건 기술적으론 맞지만, 유저 입장에선 이해가 잘 안가는 상황이라.. isLoading값을 사용해야 한다.
isLoading은 계산된 값인데, 다음과 같다.
isLoading === isPending && isFetching
즉 데이터가 없고, 실제 데이터 페칭이 이뤄지고 있냐에 대한 플래그다.
Mutation 반환값들의 상태
- status === "idle" 뮤테이션이 idle한 상태
- status === "pending" 뮤테이션이 진행중인 상태
- status === "error" 뮤테이션이 에러가 난 상태
- status === "success" 뮤테이션이 성공했고 데이터를 사용 가능할때
여기서 가장 주의해야 할 점은 뮤테이션은 데이터 자체에 대해서는 관심을 두지 않는다. 오직 뮤테이션의 진행 상황에 대해서만 관심을 둔다. 따라서 useQuery의 반환값이 데이터에 대한 상태와 데이터 페칭에 대한 상태 두가지 범주로 나뉜것에 비해 뮤테이션은 뮤테이션의 진행 상황에 대한 상태만을 반환한다.
위와 같은 이유 때문에 뮤테이션의 side effects를 사용해야 뮤테이션 이후에 데이터를 다시 가져올 수 있게 한다.
Mutation Side Effects
useMutation({
mutationFn: addTodo,
onMutate: (variables) => {
// A mutation is about to happen!
// Optionally return a context containing data to use when for example rolling back
return { id: 1 }
},
onError: (error, variables, context) => {
// An error happened!
console.log(`rolling back optimistic update with id ${context.id}`)
},
onSuccess: (data, variables, context) => {
// Boom baby!
},
onSettled: (data, error, variables, context) => {
// Error or success... doesn't matter!
},
})
가장 일반적인 사용 예시는 아마도 onSuccess 에서 invaldiateQueries를 사용하는 경우일 것이다.
위에도 언급했지만 뮤테이션은 데이터 자체에 대해서는 관여하지 않는다. (아마 useQuery와의 관심사의 분리를 위해 의도적으로 이렇게 만들지 않았나 싶다)
useMutation({
mutationFn: addEvent,
onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({queryKey: ['events']});
},
})
그럼 invalidateQueries는 뭘까?
queryClient.invalidateQueries
invalidateQueries 메서드는 쿼리를 invalidate하고 refetching을 트리거한다.
그럼 쿼리가 invalidate 되면 어떤 일이 벌어질까?
- 쿼리의 데이터가 stale 해진다.
- 쿼리가 active 상태라면 백그라운드에서 refetch를 한다.
쿼리가 active 상태라는 것은 쿼리가 useQuery에 의해 렌더링 되고 있는 상태를 의미한다.
즉 위의 뮤테이션의 사이드 이펙트의 가장 일반적인 사용 예제를 보면,
뮤테이션이 성공할 경우 특정 쿼리를 invalidate함으로써 데이터 refetching을 트리거한다. 뮤테이션은 데이터 자체에는 관심이 없기 때문에 이런 과정을 통해 뮤테이션이 완료된 이후의 데이터를 다시 받아오게 함으로써 서버 상태를 동기화한다.
그리고 사실 좀더 유용한 기법이 있어서 소개하자면, 쿼리의 특정 키를 굳이 기입하기 보다 렌더링 되어있는 쿼리들 즉, active상태의 쿼리들을 모두 invalidate하는 방법도 있다.
queryClient.invalidateQueries({ active: true });
정리
가장 기초적인 리액트 쿼리의 사용법을 이해하면서 리액트 쿼리가 사용하는 상태들에 대해 정리해봤다. 사실상 redux 이후에 de facto standard가 된 것 같은 느낌이기도 하고, 서버 상태는 최대한 분리하는게 요즘의 best practice인듯 하니 좀 더 고급 기능이나, 동작 원리에 대해서도 추후 작성 예정이다.