React Query basic
reference: https://jforj.tistory.com/search/React%20Query
useQuery
useQuery는 React Query를 이용해 서버로부터 데이터를 조회해올 때 사용합니다.
※ 데이터 조회가 아닌 데이터 변경 작업을 할 때는 **useMutation**을 사용합니다. 데이터베이스로 비유하자면 select를 할 때 사용
• queryKey: queryKey의 역할은 React-Query가 query 캐싱을 관리할 수 있도록 도와줌
• queryFn
// 1
const res = useQuery(queryKey, queryFn);
// 2
const res = useQuery({
queryKey: queryKey,
queryFn: queryFn
});option값
// 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 // 제한 없음
});refetchOnWindowFocus 전역설정
방법 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(
{
defaultOptions: {
queries: {
refetchOnWindowFocus: false, // window focus 설정
}
}
}
); // queryClient 생성
ReactDom.render(
// App에 QueryClient 제공
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>,
document.querySelector('#root')
);방법 2
// 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 자동실행 설정
ex)
if(id) {
const res = axios.get('<http://localhost:8080/person>', {
params: {
id: id,
}
})
}useQuery에서는 if문을 사용하지 않고 useQuery에서 제공해주는 query 자동 실행 설정을 통해 동일한 결과를 만들어 줄 수 있다.
// 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 // 코드 자동 실행 설정
});useMutation
useMutation은 React Query를 이용해 서버에 데이터 변경 작업을 요청할 때 사용합니다.※ 데이터 조회를 할 때는 useQuery를 사용데이터베이스로 비유하자면 insert, update, delete가 모두 포함
사용형태
// 1
const savePerson = useMutation(mutationFn);
// 2
const savePerson = useMutation({
mutationFn: mutationFn
})mutationFn은 mutation Function으로 promise 처리가 이루어지는 함수입니다.
다른 말로는 axios를 이용해 서버에 API를 요청하는 부분
// 1
const savePerson = useMutation((person: Iperson) => axios.post('/savePerson', person));
// 2
const savePerson = useMutation({
mutationFn: (person: Iperson) => axios.post('/savePerson', person)
})mutate
mutate는 useMutation을 이용해 작성한 내용들이 실행될 수 있도록 도와주는 trigger 역할
즉, useMutation을 정의 해둔 뒤 이벤트가 발생되었을 때 mutate를 사용
const savePerson = useMutation((person: Iperson) => axios.post('<http://localhost:8080/savePerson>', person)); // useMutate 정의
const onSavePerson = () => {
savePerson.mutate(person); // 데이터 저장
}
<Person.SaveButton onClick={onSavePerson}>저장</Person.SaveButton>onSuccess, onError, onSettled
일반적으로 서버에 데이터 변경 요청을 하게 되면 변경 요청이 성공할 경우에 추가적인 액션을 할 수 있도록 코드를 작성.
이런 상황은 useMutation을 사용할 때도 동일하게 적용.
async/await을 사용할 때는 보통 다음과 같이 결괏값이 있는지를 확인한 뒤 추가 작업을 수행할 수 있는 코드를 작성
try {
const res = await axios.post('<http://localhost:8080/savePerson>', person);
if(res) {
console.log('success');
}
} catch(error) {
console.log('error');
} finally {
console.log('finally');
}useMutation 을 사용하면 다음과 같이 사용
// 1
const savePerson = useMutation((person: Iperson) => axios.post('<http://localhost:8080/savePerson>', person), {
onSuccess: () => { // 요청이 성공한 경우
console.log('onSuccess');
},
onError: (error) => { // 요청에 에러가 발생된 경우
console.log('onError');
},
onSettled: () => { // 요청이 성공하든, 에러가 발생되든 실행하고 싶은 경우
console.log('onSettled');
}
});
// 2
const savePerson = useMutation({
mutationFn: (person: Iperson) => axios.post('/savePerson', person),
onSuccess: () => { // 요청이 성공한 경우
console.log('onSuccess');
},
onError: (error) => { // 요청에 에러가 발생된 경우
console.log('onError');
},
onSettled: () => { // 요청이 성공하든, 에러가 발생되든 실행하고 싶은 경우
console.log('onSettled');
}
})onSettled는 finally 구문처럼 요청이 성공하든 에러가 발생되든 상관없이 마지막에 실행되는 구간
onSuccess, onError, onSettled는 useMutation을 정의할 때만 사용할 수 있는 것이 아니라 mutate에서도 사용이 가능합니다.
const onSavePerson = () => {
savePerson.mutate(person, {
onSuccess: () => { // 요청이 성공한 경우
console.log('onSuccess');
},
onError: (error) => { // 요청에 에러가 발생된 경우
console.log('onError');
},
onSettled: () => { // 요청이 성공하든, 에러가 발생되든 실행하고 싶은 경우
console.log('onSettled');
}
}); // 데이터 저장
}invalidateQueries
useQuery에서 사용되는queryKey의 유효성을 제거해주는 목적으로 사용queryKey의 유효성을 제거해주는 이유는 서버로부터 다시 데이터를 조회해오기 위함정해진 시간이 도달하지 않으면 새로운 데이터가 적재되었더라도
useQuery는 변동 없이 동일한 데이터를 화면에 보여줄 것입니다.결국 사용자 입장에서는 데이터 생성이 제대로 되었는지에 대한 파악이 힘들기 때문에 혼란을 겪을 수 있게 됩니다.
해당 상황을 해결해줄 수 있는 것이 바로
invalidateQueries입니다.데이터를 저장할 때
invalidateQueries를 이용해useQuery가 가지고 있던queryKey의 유효성을 제거해주면 캐싱되어있는 데이터를 화면에 보여주지 않고 서버에 새롭게 데이터를 요청하게 됩니다.결국 데이터가 새롭게 추가되었을 때 다시 서버에서 데이터를 가져오게 되면서 추가한 데이터까지 화면에서 확인할 수 있게 됩니다.
const queryClient = useQueryClient(); // 등록된 quieryClient 가져오기
const savePerson = useMutation((person: Iperson) => axios.post('<http://localhost:8080/savePerson>', person), {
onSuccess: () => { // 요청이 성공한 경우
console.log('onSuccess');
queryClient.invalidateQueries('persons'); // queryKey 유효성 제거
},
onError: (error) => { // 요청에 에러가 발생된 경우
console.log('onError');
},
onSettled: () => { // 요청이 성공하든, 에러가 발생되든 실행하고 싶은 경우
console.log('onSettled');
}
}); // useMutate 정의setQueryData
invalidateQueries를 사용하지 않고도 데이터를 업데이트해줄 수 있는 방법은 있습니다.setQueryData를 활용하면 되는데setQueryData는 기존에queryKey에 매핑되어 있는 데이터를 새롭게 정의해줍니다.
const queryClient = useQueryClient(); // 등록된 quieryClient 가져오기
const savePerson = useMutation((person: Iperson) => axios.post('<http://localhost:8080/savePerson>', person), {
onSuccess: () => { // 요청이 성공한 경우
console.log('onSuccess');
queryClient.setQueryData('persons', (data) => {
const curPersons = data as AxiosResponse<any, any>; // persons의 현재 데이터 확인
curPersons.data.push(person); // 데이터 push
return curPersons; // 변경된 데이터로 set
})
},
onError: (error) => { // 요청에 에러가 발생된 경우
console.log('onError');
},
onSettled: () => { // 요청이 성공하든, 에러가 발생되든 실행하고 싶은 경우
console.log('onSettled');
}
}); // useMutate 정의useQueries
useQueries는 React Query에서 useQuery의 동적 병렬 쿼리 작업을 위해 사용
useQueries의 사용 방법은 단순하게 useQuery를 배열로 넣어준다
const ress = useQueries([
useQuery1,
useQuery2,
useQuery3,
...
]);const res1 = useQuery(['persons'], () => axios.get('<http://localhost:8080/persons>'), {
});
const res2 = useQuery(['person'], () => axios.get('<http://localhost:8080/person>', {
params: {
id: 1
}
}));const res = useQueries([
{
queryKey: ['persons'],
queryFn: () => axios.get('<http://localhost:8080/persons>'),
},
{
queryKey: ['person'],
queryFn: () => axios.get('<http://localhost:8080/person>', {
params: {
id: 1
}
}),
}
]);useQuery보다 useQueries를 사용해야 하는 상황은 동적으로 변화하는 상황
const getPersons = (persons: Iperson[]) => {
const res = 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>
);
};Suspense
React Query의 suspense 모드를 설정하게 되면 useQuery의 status, error 등을 React.Suspense로 대체
// App.tsx
import * as React from 'react';
import Queries from './pages/queries';
const App = (): JSX.Element => {
return <Queries />;
};
export default App;// queries.tsx
import axios from 'axios';
import * as React from 'react';
import { useQuery } from 'react-query';
import styled from 'styled-components';
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;
// styled
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;
`,
};React.Suspense를 이용해 다음과 같이 변경
첫 번째는 App.tsx의 Queries Component가 React.Suspense로 덮여있다는 것
두 번째는 queries.tsx의 useQuery에 suspense 모드 설정이 추가
세 번째는 queries.tsx에서 로딩 중일 경우에 대한 코드가 삭제
// 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 axios from 'axios';
import * as React from 'react';
import { useQuery } from 'react-query';
import styled from 'styled-components';
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;
`,
};suspense 모드에서 useQueries를 사용
suspense 모드에서 useQueries를 사용해야 하는 이유는 useQuery를 병렬로 처리하여 사용하고 있을 때 만약 하나의 useQuery가 정상적으로 동작되지 않을 경우 그 이후에 실행될 useQuery에 영향 을 미치며 결과적으로 올바른 화면이 보이지 않기 때문
첫 번째 요소는 걸러지고 두 번째 요소에 해당하는 결과만 화면에 노출
import axios from 'axios';
import * as React from 'react';
import { useQueries } from 'react-query';
import styled from 'styled-components';
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;
`,
};useInfiniteQuery
useInfiniteQuery는 파라미터 값만 변경하여 동일한 useQuery를 무한정 호출할 때 사용
const res = useInfiniteQuery(queryKey, queryFn);
pageParam
pageParam은 useInfiniteQuery가 현재 어떤 페이지에 있는지를 확인할 수 있는 파라미터 값
const res = useInfiniteQuery(['infinitePerson'], ({ pageParam = 5 }) =>
axios.get('<http://localhost:8080/person>', {
params: {
id: pageParam,
},
})
);기본 값은 undefined이기 때문에 값이 없을 경우 초기값으로 5 설정
데이터를 조회해올 때 pageParam값을 api 요청할 때 파라미터 값으로 넣어 사용
getNextPageParam과 fetchNextPage
getNextPageParam과 fetchNextPage은 공통적으로 다음 페이지에 있는 데이터를 조회해올 때 사용
getNextPageParam은 다음 api를 요청할 때 사용될 pageParam값을 정할 수 있다
const res = useInfiniteQuery(
['infinitePerson'],
({ pageParam = 5 }) =>
axios.get('<http://localhost:8080/person>', {
params: {
id: pageParam,
},
}),
{
getNextPageParam: (lastPage, allPages) => {
return lastPage.data.id + 1; // 다음 페이지를 호출할 때 사용 될 pageParam
},
}
);lastPage는 useInfiniteQuery를 이용해 호출된 가장 마지막에 있는 페이지 데이터를 의미합니다.
allPages는 useInfiniteQuery를 이용해 호출된 모든 페이지 데이터를 의미
return 되는 값이 다음 페이지가 호출될 때 pageParam 값
fetchNextPage는 다음 페이지의 데이터를 호출할 때 사용
useInfiniteQuery의 return 값에 포함되며 다음과 같이 버튼을 클릭할 때 실행될 이벤트로 등록해줄 수 있다
const getPersons = () => {
const res = useInfiniteQuery(
['infinitePerson'],
({ pageParam = 5 }) =>
axios.get('<http://localhost:8080/person>', {
params: {
id: pageParam,
},
}),
{
getNextPageParam: (lastPage, allPages) => {
return lastPage.data.id + 1; // 다음 페이지를 호출할 때 사용 될 pageParam
},
}
);
// 로딩 중일 경우
if (res.isLoading) {
return <LoadingText>Loading...</LoadingText>;
}
// 결과값이 전달되었을 경우
if (res.data) {
return (
<Person.Container>
{res.data.pages.map((page) => {
const person: Iperson = page.data;
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.NextButton onClick={() => res.fetchNextPage()}>
Next
</Person.NextButton>{' '}
{/* 클릭 시 다음 페이지 호출 */}
</Person.Container>
);
}
};Infinite Scroll
import axios from 'axios';
import * as React from 'react';
import { useEffect, useRef, useState } from 'react';
// info interface
interface Info {
name: string;
phone: string;
age: number;
}
const App = (): JSX.Element => {
// state
const [infoArray, setInfoArray] = useState<Info[]>([]);
// ref
const observerRef = useRef<IntersectionObserver>();
const boxRef = useRef<HTMLDivElement>(null);
// useEffect
useEffect(() => {
getInfo();
}, []);
useEffect(() => {
observerRef.current = new IntersectionObserver(intersectionObserver); // IntersectionObserver
boxRef.current && observerRef.current.observe(boxRef.current);
}, [infoArray]);
// function
const getInfo = async () => {
const res = await axios.get('<http://localhost:8080/rest/getInfo>'); // 서버에서 데이터 가져오기
setInfoArray((curInfoArray) => [...curInfoArray, ...res.data]); // state에 추가
// console.log('info data add...');
};
// IntersectionObserver 설정
const intersectionObserver = (
entries: IntersectionObserverEntry[],
io: IntersectionObserver
) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// 관찰하고 있는 entry가 화면에 보여지는 경우
io.unobserve(entry.target); // entry 관찰 해제
getInfo(); // 데이터 가져오기
}
});
};
// style
const Wrapper = {
width: '800px',
margin: '0 auto',
};
const Box = {
border: '1px solid olive',
borderRadius: '8px',
boxShadow: '1px 1px 2px olive',
margin: '18px 0',
};
const BoxTable = {
borderSpacing: '15px',
};
const Title = {
fontWeight: 700,
};
return (
<div style={Wrapper}>
{infoArray.map((info, index) => {
if (infoArray.length - 5 === index) {
// 관찰되는 요소가 있는 html, 아래에서 5번째에 해당하는 박스를 관찰
return (
<div style={Box} ref={boxRef} key={index}>
<table style={BoxTable}>
<tbody>
<tr>
<td style={Title}>이름</td>
<td>{info.name}</td>
</tr>
<tr>
<td style={Title}>전화번호</td>
<td>{info.phone}</td>
</tr>
<tr>
<td style={Title}>나이</td>
<td>{info.age}</td>
</tr>
</tbody>
</table>
</div>
);
}
// 관찰되는 요소가 없는 html
return (
<div style={Box} key={index}>
<table style={BoxTable} key={index}>
<tbody>
<tr>
<td style={Title}>이름</td>
<td>{info.name}</td>
</tr>
<tr>
<td style={Title}>전화번호</td>
<td>{info.phone}</td>
</tr>
<tr>
<td style={Title}>나이</td>
<td>{info.age}</td>
</tr>
</tbody>
</table>
</div>
);
})}
</div>
);
};
export default App;Last updated