리액트 쿼리를 사용할 때, 단건의 API 요청이 필요한 경우 useQuery 훅을 통해 여러가지 옵션들과 쿼리 데이터를 활용할 수 있는데, 만약 병렬적인 다건의 API 요청이 필요한 경우는 어떤 훅을 사용해야 할까?
이와 같은 상황에서 사용할 수 있는 훅이 바로 useQueries이다.
최근 기능을 구현하며 useQueries를 사용할 일이 있었는데, 이를 간단하게 정리해보려고 한다.
useQueries 훅을 사용하면 useQuery 훅을 통해 반환되는 쿼리 데이터가 배열로 반환된다.
즉, useQuery 훅의 반환 값에 대한 인터페이스가 아래와 같다면
UseQueryResult<API응답값>
useQueries 훅의 반환 값에 대한 인터페이스는 다음과 같다.
UseQueryResult<API응답값>[]
useQueries의 사용법은 공식 문서에 설명되어 있다시피 아래와 같다.
const results = useQueries({
queries: [
{ queryKey: ['post', 1], queryFn: fetchPost, staleTime: Infinity},
{ queryKey: ['post', 2], queryFn: fetchPost, staleTime: Infinity},
...
]
})
queries 배열에 들어가는 요소는 useQuery에 전달되는 argument와 동일한 인터페이스를 갖는다. 즉, queryKey를 필수적으로 가져야 하고, 일반적인 경우 queryFn을 가져야 하며, useQuery에 전달할 수 있는 옵션들을 설정해줄 수 있다(ex: staleTime, enabled 등).
'useQuery로 병렬적인 다건의 API 요청도 커버할 수 있지 않나?' 하는 의문이 생길 수도 있다.
다음과 같이 말이다.
export const SomeComponentA = () => {
const queryA = useQuery({ queryKey: ['keyA', 'a'], queryFn: someFnA });
const queryB = useQuery({ queryKey: ['keyA', 'b'], queryFn: someFnB });
const queryC = useQuery({ queryKey: ['keyA', 'c'], queryFn: someFnC });
...
}
export const SomeComponentB = () => {
const queryA1 = useQuery({ queryKey: ['keyB', 'a', 1], queryFn: () => someFnA(1) });
const queryA2 = useQuery({ queryKey: ['keyB', 'a', 2], queryFn: () => someFnA(2) });
const queryA3 = useQuery({ queryKey: ['keyB', 'a', 3], queryFn: () => someFnA(3) });
...
}
하지만 위의 코드만 봐도 필요한 API 요청이 늘어날때마다 코드가 무한으로 증식하고, API 요청이 동적으로 변경되는 것이 아니라 정적인 상태로 한정된다는 것을 알 수 있다.
만약, runtime에 SomeComponentA가 someFnA, someFnB, someFnC 요청 뿐만 아니라 someFnD, someFnE 요청을 필요로 한다면 어떻게 대응해야 할 것인가?
SomeComponentB가 someFnA(1), someFnA(2), someFnA(3) 요청 뿐만 아니라 someFnA(4), someFnA(5) 요청을 필요로 한다면 어떻게 해야할 것인가?
이렇게 queryKey와 queryFn가 runtime에 동적으로 변경될 수 있는 환경에서 병렬적으로 쿼리 데이터를 다뤄야 한다면 useQuery 훅이 아닌, useQueries 훅을 사용해야 한다.
예를 통해서 상황을 이해해보자.
A사, B사, C사의 신문을 모아 볼 수 있는 사이트가 있다고 하자.
제출 버튼을 클릭하면 A, B, C 사에서 가장 최신에 발행된 신문을 받아볼 수 있는 기능이 있다고 하자.
그러면 신문을 받아보는 페이지에서는 다음과 같이 useQuery 훅만 사용해서 기능을 구현할 수 있을 것이다.
export const Page = () => {
const aQuery = useQuery({ queryKey: ['news', 'a'], queryFn: getLatestNewsA });
const bQuery = useQuery({ queryKey: ['news', 'b'], queryFn: getLatestNewsB });
const cQuery = useQuery({ queryKey: ['news', 'c'], queryFn: getLatestNewsC });
...
}
만약, 아래와 같이 유저가 신문사별로 부수를 지정하고, 날짜를 지정할 수 있다면 어떻게 될까?
이때는 runtime에 동적으로 변화하는 부수와 parameter로 넘겨줘야 하는 날짜의 변수로 인해 useQuery로는 불러올 API의 개수와 종류를 특정지을 수 없다.
이런 상황에서 useQueries가 필요하고, 다음과 같이 구현할 수 있을 것이다.
먼저 제출 버튼을 클릭 하면 유저의 입력 값을 상태로 저장한다.
const requestedPapers = [
{type: 'a', date: '23-05-01'},
{type: 'b', date: '23-05-15'},
{type: 'b', date: '23-05-17'},
{type: 'c', date: '23-05-25'}
]
그리고 저장된 유저의 입력 값을 바탕으로 쿼리 데이터를 불러온다.
export const Page = () => {
const newsQueries = useQueries({
queries: requestedPapers.map((p) => {
if (p.type === 'a') {
return {
queryKey: ['news', 'a', { date: p.date }],
queryFn: () => getNewsA(p.date)
};
}
if (p.type === 'b') {
return {
queryKey: ['news', 'b', { date: p.date }],
queryFn: () => getNewsB(p.date)
};
}
// p.type === 'c'
return {
queryKey: ['news', 'c', { date: p.date }],
queryFn: () => getNewsC(p.date)
};
})
});
...
}
위와 같이 runtime에 동적으로 변화하는 값에도 적절하게 queryKey와 queryFn을 할당함으로써 쿼리 데이터를 활용할 수 있다.
주로 서비스에 useQuery가 필요한 순간이 대부분이었는데, runtime에서의 입력 값 변화, 병렬적인 다건 API 요청에 대한 요구조건이 생기면서 useQueries를 사용하게 되었다. 다건으로 API를 요청하는 만큼 최적화를 위해 캐싱 조건을 어떻게 설정할 것인지 더 고민해볼 수 있었고, 추가적인 API 요청을 제어하기 위해 queryKey 설계를 더 촘촘하게 해볼 수 있었다. 단순히 병렬적인API 요청이 가능하다에서 그치는 것이 아니라 단일 entry point를 갖는데, 여러 성격의 input이 들어올 수 있는 환경에서, 성격에 따라 다양한 API를 호출하게끔 할 수 있다는 측면에서 useQueries의 효용을 많이 느낄 수 있었다.