useQuery란?
useQuery
는 React Query를 이용해 서버로부터 데이터를 조회해올 때 사용한다.
*데이터 조회가 아닌 데이터 변경 작업을 할 때는 useMutation
을 사용한다.
useQuery는 다음과 같은 형태로 사용된다.
// 1
const res = useQuery(queryKey, queryFn)
// 2
const res = useQuery({
queryKey: queryKey,
queryFn: queryFn
})
시작하기
react-query를 설치해주고, app.tsx를 다음과 같이 변경해준다.
yarn add react-query
// app.tsx
import { QueryClient, QueryClientProvider } from "react-query";
import Main from "./components/Main";
const queryClient = new QueryClient();
function App() {
return (
<div className="App">
<QueryClientProvider client={queryClient}>
<Main />
</QueryClientProvider>
</div>
);
}
export default App;
QueryClient() 를 이용하여 queryClient 인스턴스를 생성해준다. 그리고 이것을 QueryClientProvider의 props로 전달해준다.
queryKey
queryKey는 useQuery마다 부여되는 고유 Key 값이다.
해당 Key 값은 단순하게 문자열로 사용될 수도 있고 또한 배열의 형태로도 사용될 수 있다.
// 문자열
const res = useQuery('persons', queryFn)
// 배열
const res = useQuery(['persons'], queryFn)
const res = useQuery(['persons', 'addId'], queryFn)
const res = useQuery(['addId','persons'], queryFn)
const res = useQuery(['persons', {type: 'add', name: 'Id'}], queryFn)
문자열은 현재 "persons" 라는 문자열이 queryKey로 사용되고 있다.
문자열로 작성된 경우에는 자동으로 길이가 1인 배열로 인식하기 때문에 결과적으로 ["persons"] 라고 입력한 것과 동일한 queryKey로 작성된다.
또한 ["persons", "addId"] 와 ["addId", "persons"]는 queryKey가 동일해보이지만 React Query에게는 동일하지 않은 queryKey로 인식된다.
이는 queryKey가 할당될 때 배열에 입력되는 순서도 보장해주기 때문이다.
역할
queryKey의 역할은 React Query가 query 캐싱을 관리할 수 있도록 도와준다.
import * as React from 'react';
import axios from 'axios';
import { useQuery } from 'react-query';
const Query = (): JSX.Element => {
const getPersons1 = () => {
const res1 = useQuery(['persons'], queryFn1);
}
const getPersons2 = () => {
const res2 = useQuery(['persons'], queryFn2);
}
return (
<div>
{getPersons1()}
{getPersons2()}
</div>
)
}
export default Query;
위의 코드에서 res1과 res2가 동일한 queryKey를 사용하며 서버에 있는 데이터를 조회해오려고 하고 있다.
일반적인 상황에서는 res1과 res2에 대한 모든 요청이 이루어지게 되므로 서버에 2개의 request가 전달될 것이다.
하지만 해당 코드에서는 서버에 1개의 request만 전달이 된다.
res1에서 request를 서버에 전달하게 되면 res2에서는 이미 동일한 queryKey에 대한 결괏값이 있기 때문에 추가 요청을 하지 않고 res1의 결과를 그대로 가져와 사용하기 때문이다.
또한 queryFn이 다르게 정의되어 있더라도 res2에서는 res1의 결과를 그대로 전달받기 때문에 queryFn1이 처리된 결과를 확인할 수 있다.
import * as React from 'react';
import axios from 'axios';
import { useQuery } from 'react-query';
const Query = (): JSX.Element => {
const getPersons1 = () => {
const res1 = useQuery(['persons'], queryFn1);
}
const getPersons2 = () => {
const res2 = useQuery(['persons']);
}
return (
<div>
{getPersons1()}
{getPersons2()}
</div>
)
}
export default Query;
queryFn
queryFn은 query Function으로 promise 처리가 이루어지는 함수라고 생각하면 된다.
다른 말로는 axios 를 이용해 서버에 API 요청하는 코드라고 생각할 수 있다.
그렇기 때문에 useQuery는 결과적으로 다음과 같은 형태로 코드가 작성된다.
// 1
const res = useQuery(['persons'], () => axios.get('http://localhost:8080/persons'));
// 2
const res = useQuery({
queryKey: ['persons'],
queryFn: () => axios.get('http://localhost:8080/persons')
});
staleTime과 cacheTime
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 Query = (): JSX.Element => {
const getPersons = () => {
const res = useQuery(['persons'], () => axios.get('http://localhost:8080/persons')); // API 호출
// 로딩 중일 경우
if(res.isLoading) {
return (
<LoadingText>Loading...</LoadingText>
)
}
// 결과값이 전달되었을 경우
if(res.data) {
const persons: Iperson[] = res.data.data;
return (
<Person.Container>
{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 Query;
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;
`
}
서버를 실행시킨 뒤 작성한 파일을 화면에 띄우면 다음과 같다.
여기서 Network탭을 확인해보면 단순히 탭 페이지 전환만 있음에도 불구하고 persons를 지속적으로 호출한다.
이처럼 호출이 지속적으로 발생되는 이유는 자동으로 refetch가 이루어지고 있기 때문이다.
staleTime
refetch가 발생되는 이유는 queryKey에 매핑되는 데이터가 fresh 하지 않고 stale 해졌기 때문이다.
stale은 탁한, 신선하지 않은 이라는 의미를 가지는데 데이터가 stale 해졌다는 것은 결국 오래된 데이터라고 생각할 수 있다.
stale 한 데이터를 사용자에게 보여주는 것은 유의미하지 않다고 React Query는 판단하고 fresh한 데이터를 요구하게 된다.
그리고 결국 서버로부터 fresh한 데이터를 전달받기 위해 refetch가 이루어진다.
개발자의 입장에서는 데이터가 stale 하지 않다고 생각할 수 있지만 React Query는 계속해서 refetch를 수행한다.
왜냐하면 staleTime의 default값이 0초이기 때문이다.
한 번 데이터를 조회해오면 그 순간 바로 해당 데이터는 stale한 데이터이기 때문에 refetch가 계속해서 발생되는 것이다.
cacheTime
이런 staleTime과 유사한 역할을 수행하는 것에는 cacheTime이 있다.
cacheTime은 캐싱 처리가 이루어지는 시간을 의미한다.
cacheTime은 default값으로 5분으로 설정되어 있다.
그렇기 때문에 queryKey에 매핑되는 데이터가 사용되지 않는 시점을 기준으로 5분이 지나지 않은 상태에서 해당 queryKey를 다시 호출할 경우 이전에 가져왔던 데이터를 다시 보여주게 된다.
하지만 5분이 지나게 되면 캐시 가비지 콜렉터 타이머가 실행되며 기존 데이터는 삭제 처리가 이루어지고 queryKey를 다시 호출하게 되면 서버에 다시 데이터를 요청하게 된다.
즉 useQuery에는 staleTime, cacheTime 두 개념이 모두 존재하기 때문에 둘 중 하나라도 만족되지 않으면 서버에 다시 데이터를 요청하게 된다.
그렇기 때문에 두 설정을 모두 고려하여 코드를 구현해야 된다.
staleTime과 cacheTime의 설정 방법은 useQuery 작성 시 다음과 같이 설정해줄 수 있다.
*작성되는 시간은 ms 단위이기 때문에 1000 = 1초 라고 생각하면 된다.
// 1
const res = useQuery(['persons'], () => axios.get('http://localhost:8080/persons'), {
staleTime: 5000, // 5초
cacheTime: Infinity, // 제한 없음
});
// 2
const res = useQuery({
queryKey: ['persons'],
queryFn: () => axios.get('http://localhost:8080/persons'),
staleTime: 5000, // 5초
cacheTime: Infinity // 제한 없음
});
refetch window focus 설정
staleTime에 대한 개념을 알게 됐더라도 조금 낯선 부분은 단순 페이지 전환만으로 refetch가 이루어진다는 것이다.
이런 동작이 수행되는 이유는 default로 window focus 설정이 true로 되어 있기 때문이다.
어떤 상황에서는 이런 기능이 더 효율적으로 사용될 수는 있지만 또 다른 상황에서는 필요없는 기능일 수도 있다.
그럴 때는 window focus 설정을 false로 변경하여 staleTime이 지났더라도 focus가 다시 되는 것만으로 refetch가 발생되지 않게 설정해줄 수 있다.
참고적으로 window focus 설정이 false 로 변경된 후 staleTime이 지났을 때 refetch 되는 경우로는 다른 화면으로 이동되었다가 다시 현재 화면으로 되돌아오는 케이스가 있다.
설정하는 방법은 2가지가 있다.
1. 전역적으로 설정하는 방법
import * as React from 'react';
import ReactDom from 'react-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import App from './App';
const queryClient = new QueryClient(); // queryClient 생성
ReactDom.render(
// App에 QueryClient 제공
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>,
document.querySelector('#root')
);
App에 QueryClient를 제공하기 위해 위와 같이 설정된 코드가 있을 것이다.
여기서 QueryClient를 생성할 때 다음과 같이 window focus 설정을 false로 해주면 전역적으로 적용할 수 있다.
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: {
refetchOnWindowFocus: false, // window focus 설정
}
}
}
); // queryClient 생성
ReactDom.render(
// App에 QueryClient 제공
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>,
document.querySelector('#root')
);
2. useQuery마다 설정하는 방법
useQuery를 생성할 때 다음과 같이 각각 window focus 설정을 변경해줄 수 있다.
// 1
const res = useQuery(['persons'], () => axios.get('http://localhost:8080/persons'), {
refetchOnWindowFocus: false // window focus 설정
});
// 2
const res = useQuery({
queryKey: ['persons'],
queryFn: () => axios.get('http://localhost:8080/persons'),
refetchOnWindowFocus: false // window focus 설정
});
query 자동 실행 설정
코드를 구현하다 보면 다음과 같이 조건이 맞을 떄만 서버에 데이터를 요청하고 조건이 맞지 않으면 코드 실행을 막는 경우가 있다.
if(id) {
const res = axios.get('http://localhost:8080/person', {
params: {
id: id,
}
})
}
useQuery를 사용할 떄도 동일한 상황이 생길 수 있다.
하지만 useQuery에서는 if문을 사용하지 않고 useQuery에서 제공해주는 query 자동 실행 설정을 통해 동일한 결과를 만들어 줄 수 있다.
// options에 enabled 설정을 추가해줄 수 있다.
useQuery(queryKey, queryFn, options)
// 1
const res = useQuery(['person', id], () => axios.get('http://localhost:8080/person', {
params: {
id: id,
}
}), {
enabled: !!id // 코드 자동 실행 설정
});
// 2
const res1 = useQuery({
queryKey: ['person', id],
queryFn: () => axios.get('http://localhost:8080/person', {
params: {
id: id,
}
}),
enabled: !!id // 코드 자동 실행 설정
});
enabled의 default 값은 true로 되어 있다.
하지만 위와 같이 id값이 존재하지 않을 경우 false를 변경해줌으로써 자동 실행을 막을 수 있게 도와준다.
출처