안녕하세요. J4J입니다.
이번 포스팅은 React Query의 useQueries에 대해 적어보는 시간을 가져보려고 합니다.
들어가기에 앞서
react query의 기능 업데이트가 주기적으로 이루어졌고 최근 v5로 넘어오면서 여러 가지 변경점들이 발생되었습니다.
최근 변경점에 대해서도 확인하고 싶으신 분들은 아래 글들을 참고 부탁드립니다.
[React] react-query v5 변경점 알아보기 (1) - 공통 기능 변경
[React] react-query v5 변경점 알아보기 (2) - query 공통 기능 변경
[React] react-query v5 변경점 알아보기 (3) - useQueries 기능 변경
useQueries란?
useQueries는 React Query에서 useQuery의 동적 병렬 쿼리 작업을 위해 사용됩니다.
그리고 여기서 말하는 동적 병렬 쿼리 작업은 병렬 쿼리 작업을 수행을 하지만 상황에 따라 쿼리 작업이 유동적으로 변하는 것을 의미합니다.
useQueries의 사용 방법은 단순하게 useQuery를 배열로 넣어준다고 생각하시면 됩니다.
그렇기 때문에 다음과 같은 형태로 사용됩니다.
const ress = useQueries([
useQuery1,
useQuery2,
useQuery3,
...
]);
useQuery와 비교하기 (1) - Manual
데이터 조회할 때 주로 사용하는 useQuery도 병렬 쿼리 작업 수행이 가능합니다.
병렬 쿼리 작업이 수행되게 코드를 작성하는 방법은 다음과 같이 단순하게 순서대로 작성해주면 됩니다.
const res1 = useQuery(['persons'], () => axios.get('http://localhost:8080/persons'), {
});
const res2 = useQuery(['person'], () => axios.get('http://localhost:8080/person', {
params: {
id: 1
}
}));
이와 동일한 작업이 수행될 수 있게 useQueries로 다음과 같이 변경해줄 수 있습니다.
const res = useQueries([
{
queryKey: ['persons'],
queryFn: () => axios.get('http://localhost:8080/persons'),
},
{
queryKey: ['person'],
queryFn: () => axios.get('http://localhost:8080/person', {
params: {
id: 1
}
}),
}
]);
이처럼 단순하게만 사용하는 관점에서는 사실 둘중 어떤 것을 사용해도 차이가 없기 때문에 취향에 맞게 사용하셔도 무방합니다.
공식 문서에서도 이렇게 Manual 한 Query를 작성할 때는 useQuery를 원하는 만큼 나열식으로 작성하라고 말하고 있습니다.
useQuery와 비교하기 (2) - Dynamic
useQuery보다 useQueries를 사용해야 하는 상황은 동적으로 변화하는 상황입니다.
예시로 다음과 같은 코드를 작성해볼 수 있습니다.
const getPersons = (persons: Iperson[]) => {
const ress = useQueries(
persons.map((person) => {
return {
queryKey: ['person', person.id],
queryFn: () => axios.get('http://localhost:8080/person', {
params: {
id: person.id
}
})
}
})
)
return (
<Person.Container>
{ress && ress.map((res) => {
const person: Iperson = res.data && res.data.data;
return (
person &&
<Person.Box key={person.id}>
<Person.Title>{person.id}.</Person.Title>
<Person.Text>{person.name}</Person.Text>
<Person.Text>({person.age})</Person.Text>
</Person.Box>
)
})}
</Person.Container>
)
}
getPersons의 파라미터로 넘어오는 persons의 값이 항상 동일하지 않기에 렌더링이 될 때 어떤 상황에서는 길이가 1이고, 또 다른 상황에서는 길이가 2일 수도 있습니다.
이런 상황에서는 useQuery를 이용해 미리 나열을 해둘 수 없습니다.
그렇기 때문에 이런 동적으로 변화되는 상황 속에서는 useQueries를 이용해 더 세련되게(?) 코드를 작성해줄 수 있습니다.
또한 공식 문서에서도 다음과 같이 표현이 되고 있습니다.
you cannot use manual querying since that would violate the rules of hooks.
hook의 규칙에 위반되기 때문에 Manual한 방식으로 사용할 수 없다고 하는데... 사실 어떤 규칙에 위반되는지는 정확히 모르겠지만 위의 상황을 useQuery로만 풀어보라고 하면 구현하기 막막하게 느껴질 것입니다.
Suspense
공식 문서에 따르면 useQuery말고 useQueries를 사용해야 하는 또 다른 상황이 존재합니다.
그건 바로 useQuery를 사용하면서 suspense 모드로 동작시킬 때입니다.
여기서 말하는 React Query의 suspense 모드를 설정하게 되면 useQuery의 status, error 등을 React.Suspense로 대체해줍니다.
간단하게 코드로 예시를 들기 위해 App.tsx 파일과 queries.tsx 파일을 다음과 같이 작성해보겠습니다.
// App.tsx
import * as React from 'react';
import Queries from './pages/queries';
const App = (): JSX.Element => {
return (
<Queries />
)
}
export default App;
// queries.tsx
import * as React from 'react';
import axios from 'axios';
import styled from 'styled-components';
import { useQuery } from 'react-query';
interface Iperson {
id: number;
name: string;
phone: string;
age: number;
}
const Queries = (): JSX.Element => {
const getPersons = () => {
const res = useQuery(['persons'], () => axios.get('http://localhost:8080/persons'));
// 로딩 중일 경우
if(res.isLoading) {
return (
<LoadingText>Queries Loading...</LoadingText>
)
}
// 결과값이 전달되었을 경우
const persons: Iperson[] = res.data && res.data.data;
return (
<Person.Container>
{persons && persons.map((person) => {
return (
<Person.Box key={person.id}>
<Person.Title>{person.id}.</Person.Title>
<Person.Text>{person.name}</Person.Text>
<Person.Text>({person.age})</Person.Text>
</Person.Box>
)
})}
</Person.Container>
)
}
return (
<Wrapper>
{getPersons()}
</Wrapper>
)
}
export default Queries;
const Wrapper = styled.div`
max-width: 728px;
margin: 0 auto;
`;
const LoadingText = styled.h3`
text-align: center;
`;
const Person = {
Container: styled.div`
padding: 8px;
`,
Box: styled.div`
border-bottom: 2px solid olive;
`,
Title: styled.h2`
display: inline-block;
margin: 0 12px;
line-height: 48px;
`,
Text: styled.span`
margin: 0 6px;
`
}
이렇게 코드를 작성하면 getPersons 안에 있는 useQuery 요청이 처리되는 동안 Queries Loading... 이라는 문구가 화면에 보이게 됩니다.
이와 유사한 상황을 React.Suspense를 이용해 다음과 같이 변경해줄 수 있습니다.
// App.tsx
import * as React from 'react';
import Queries from './pages/queries';
const App = (): JSX.Element => {
return (
<React.Suspense fallback={<div>App Loading...</div>}>
<Queries />
</React.Suspense>
)
}
export default App;
// queries.tsx
import * as React from 'react';
import axios from 'axios';
import styled from 'styled-components';
import { useQuery } from 'react-query';
interface Iperson {
id: number;
name: string;
phone: string;
age: number;
}
const Queries = (): JSX.Element => {
const getPersons = () => {
const res = useQuery(['persons'], () => axios.get('http://localhost:8080/persons'), {
suspense: true // suspense 모드 설정
});
// 결과값이 전달되었을 경우
const persons: Iperson[] = res.data && res.data.data;
return (
<Person.Container>
{persons && persons.map((person) => {
return (
<Person.Box key={person.id}>
<Person.Title>{person.id}.</Person.Title>
<Person.Text>{person.name}</Person.Text>
<Person.Text>({person.age})</Person.Text>
</Person.Box>
)
})}
</Person.Container>
)
}
return (
<Wrapper>
{getPersons()}
</Wrapper>
)
}
export default Queries;
const Wrapper = styled.div`
max-width: 728px;
margin: 0 auto;
`;
const Person = {
Container: styled.div`
padding: 8px;
`,
Box: styled.div`
border-bottom: 2px solid olive;
`,
Title: styled.h2`
display: inline-block;
margin: 0 12px;
line-height: 48px;
`,
Text: styled.span`
margin: 0 6px;
`
}
변경이 이루어진 부분은 3곳이 있습니다.
첫 번째는 App.tsx의 Queries Component가 React.Suspense로 덮여있다는 것입니다.
두 번째는 queries.tsx의 useQuery에 suspense 모드 설정이 추가되었습니다.
세 번째는 queries.tsx에서 로딩 중일 경우에 대한 코드가 삭제되었습니다.
이렇게 변경점을 적용하고 새로고침을 하게 되면 useQuery 요청이 처리되는 동안 화면에는 App Loading... 이라는 문구가 보이게 됩니다.
suspense 모드를 설정하고 React.Suspense를 사용하게 되면서 로딩 상태 처리를 useQuery에서 하지 않게 된 것입니다.
추가적으로 suspense 모드는 queryClient를 등록을 위해 queryClient를 생성할 때 다음과 같이 설정하여 전역적으로도 적용시킬 수 있습니다.
import * as React from 'react';
import ReactDom from 'react-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import App from './App';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
suspense: true // suspense 모드 설정
}
}
}); // queryClient 생성
ReactDom.render(
// App에 QueryClient 제공
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>,
document.querySelector('#root')
);
suspense 모드에서 useQueries를 사용해야 하는 이유
suspense 모드에서 useQueries를 사용해야 하는 이유는 useQuery를 병렬로 처리하여 사용하고 있을 때 만약 하나의 useQuery가 정상적으로 동작되지 않을 경우 그 이후에 실행될 useQuery에 영향을 미치며 결과적으로 올바른 화면이 보이지 않기 때문입니다.
예를 들어 queries.tsx 파일을 다음과 같이 수정해보겠습니다.
import * as React from 'react';
import axios from 'axios';
import styled from 'styled-components';
import { useQuery } from 'react-query';
interface Iperson {
id: number;
name: string;
phone: string;
age: number;
}
const Queries = (): JSX.Element => {
const getPersons = () => {
const res1 = useQuery(['persons'], () => axios.get('http://localhost:8080/persons'), {
suspense: true
}); // API 호출
const res2 = useQuery(['person'], () => axios.get('http://localhost:8080/person', {
params: {
id: 1
}
}), {
suspense: true
}); // API 호출
// 결과값이 전달되었을 경우
const persons: Iperson[] = res1.data && res1.data.data;
const person: Iperson = res2.data && res2.data.data;
return (
<Person.Container>
{persons && persons.map((person) => {
return (
<Person.Box key={person.id}>
<Person.Title>{person.id}.</Person.Title>
<Person.Text>{person.name}</Person.Text>
<Person.Text>({person.age})</Person.Text>
</Person.Box>
)
})}
{person &&
<Person.Box>
<Person.Title>{person.id}.</Person.Title>
<Person.Text>{person.name}</Person.Text>
<Person.Text>({person.age})</Person.Text>
</Person.Box>}
</Person.Container>
)
}
return (
<Wrapper>
{getPersons()}
</Wrapper>
)
}
export default Queries;
const Wrapper = styled.div`
max-width: 728px;
margin: 0 auto;
`;
const Person = {
Container: styled.div`
padding: 8px;
`,
Box: styled.div`
border-bottom: 2px solid olive;
`,
Title: styled.h2`
display: inline-block;
margin: 0 12px;
line-height: 48px;
`,
Text: styled.span`
margin: 0 6px;
`
}
그러면 다음과 같은 화면이 나오는 것을 확인할 수 있습니다.
하지만 만약 여기서 실수로 res1에 해당되는 request url 값을 존재하지 않는 값으로 변경했다고 가정해보겠습니다.
(올바른 request url이지만 서버에서 정상 처리를 할 수 없을 때도 포함됩니다.)
const res1 = useQuery(['persons'], () => axios.get('http://localhost:8080/personssss'), {
suspense: true
}); // API 호출
비록 코드에서는 조건 값을 다 따지며 결괏값이 존재하는 것만 화면에 뿌려주도록 설정했음에도 불구하고 다음과 같은 화면을 확인할 수 있습니다.
그럼 이번엔 useQuery 대신 useQueries를 사용해보도록 하겠습니다.
다음과 같이 queries.tsx 파일을 수정해보겠습니다.
import * as React from 'react';
import axios from 'axios';
import styled from 'styled-components';
import { useQueries } from 'react-query';
interface Iperson {
id: number;
name: string;
phone: string;
age: number;
}
const Queries = (): JSX.Element => {
const getPersons = () => {
const res = useQueries([
{
queryKey: ['persons'],
queryFn: () => axios.get('http://localhost:8080/personssss'),
suspense: true
},
{
queryKey: ['person'],
queryFn: () => axios.get('http://localhost:8080/person', {
params: {
id: 1
}
}),
suspense: true
}
])
if(res) {
const persons: Iperson[] = res[0].data && res[0].data.data;
const person: Iperson = res[1].data && res[1].data.data;
return (
<Person.Container>
{persons && persons.map((person) => {
return (
<Person.Box key={person.id}>
<Person.Title>{person.id}.</Person.Title>
<Person.Text>{person.name}</Person.Text>
<Person.Text>({person.age})</Person.Text>
</Person.Box>
)
})}
{person &&
<Person.Box>
<Person.Title>{person.id}.</Person.Title>
<Person.Text>{person.name}</Person.Text>
<Person.Text>({person.age})</Person.Text>
</Person.Box>}
</Person.Container>
)
}
}
return (
<Wrapper>
{getPersons()}
</Wrapper>
)
}
export default Queries;
const Wrapper = styled.div`
max-width: 728px;
margin: 0 auto;
`;
const Person = {
Container: styled.div`
padding: 8px;
`,
Box: styled.div`
border-bottom: 2px solid olive;
`,
Title: styled.h2`
display: inline-block;
margin: 0 12px;
line-height: 48px;
`,
Text: styled.span`
margin: 0 6px;
`
}
위와 동일하게 useQueries의 첫 번째 요소의 url을 올바르지 않게 해 놨습니다.
하지만 화면에 보이는 것은 다음과 같습니다.
첫 번째 요소는 걸러지고 두 번째 요소에 해당하는 결과만 화면에 보이고 있습니다.
이와 같은 결과를 만들어내기 때문에 suspense 모드를 설정하여 사용할 때는 useQuery보다는 useQueries를 사용할 필요가 있어 보입니다.
useQueries와 suspense 사이의 반전스러운 결과
공식 문서를 참고하며 위의 결과를 확인했을 때 까지는 suspense 모드를 설정하면 useQueries를 사용해야겠구나에 대해 배울 수 있었습니다.
하지만 추가적인 테스트를 하다 보면서 이상한 점을 찾을 수 있었습니다.
useQueries를 사용하여 suspense 모드를 설정할 경우 React.Suspense가 정상적으로 동작되지 않는 것입니다.
useQuery를 사용할 때는 다음과 같이 React.Suspense가 정상 동작이 되어 App Loading... 이라는 문구가 잠시 보이는 걸 확인할 수 있었습니다.
하지만 useQueries를 사용할 때는 다음과 같이 React.Suspense가 동작되지 않아 App Loading... 문구가 보이지 않습니다.
이게 현재 잠시 동안만의 에러이면 위에서 내린 결론을 그대로 지식으로 챙겨가면 될 것 같지만, 만약 이 현상이 고쳐지지 않으면 queries와 suspense 모드를 같이 사용할 이유가 없게 됩니다.
왜냐하면 동작이 되지 않으니까요.
이게 지속된다면 query를 사용할 때 발생할 수 있는 오동작 현상이 있더라도 query와 suspense 모드를 함께 사용해야 할 것으로 보입니다.
이상으로 React Query의 useQueries에 대해 간단하게 알아보는 시간이었습니다.
읽어주셔서 감사합니다.
'SPA > React' 카테고리의 다른 글
[React] useInfiniteQuery를 이용하여 Infinite Scroll (무한 스크롤) 구현 (1) | 2022.03.14 |
---|---|
[React] React Query의 useInfiniteQuery에 대해 알아보기 (3) | 2022.03.13 |
[React] React Query의 useMutation에 대해 알아보기 (3) | 2022.03.09 |
[React] React Query의 useQuery에 대해 알아보기 (5) | 2022.03.08 |
[React] React Query에 대한 소개와 사용 환경 설정 (0) | 2022.03.07 |
댓글