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
})

mutationFnmutation 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');
    }
})

onSettledfinally 구문처럼 요청이 성공하든 에러가 발생되든 상관없이 마지막에 실행되는 구간

onSuccess, onError, onSettleduseMutation을 정의할 때만 사용할 수 있는 것이 아니라 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

useQueriesReact 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

pageParamuseInfiniteQuery가 현재 어떤 페이지에 있는지를 확인할 수 있는 파라미터 값

const res = useInfiniteQuery(['infinitePerson'], ({ pageParam = 5 }) =>
  axios.get('<http://localhost:8080/person>', {
    params: {
      id: pageParam,
    },
  })
);

기본 값은 undefined이기 때문에 값이 없을 경우 초기값으로 5 설정

데이터를 조회해올 때 pageParam값을 api 요청할 때 파라미터 값으로 넣어 사용

  • getNextPageParam과 fetchNextPage

getNextPageParamfetchNextPage은 공통적으로 다음 페이지에 있는 데이터를 조회해올 때 사용

  • 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
    },
  }
);

lastPageuseInfiniteQuery를 이용해 호출된 가장 마지막에 있는 페이지 데이터를 의미합니다.

allPagesuseInfiniteQuery를 이용해 호출된 모든 페이지 데이터를 의미

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