짧게 짚고 넘어가는 리액트쿼리 기본 개념
리액트에서 복잡한 데이터 상태 및 비동기 작업관리를 간편하게 처리할 수 있는 라이브러리
- 데이터를 비동기적으로 처리하고 데이터 캐싱, 리패칭, 뮤테이션 등과 같은 복잡한 데이터 관리 작업을 간편하게 수행
- 캐싱 및 성능 최적화 : React query 는 자동으로 데이터를 캐싱하여 네트워크 요청을 최소화 한다.
- 데이터 리로드 조작
- 무한스크롤 기능 쉽게 구현 가능
- 데이터 동기화 : 실시간 동기화를 지원하여 데이터가 변경될 때 자동으로 새로운 데이터 업데이트
- 서버 요청 관리 : 쿼리를 통해 서버요청을 효과적으로 관리 (중복요청 방지, 요청취소, 재시도)
- 그 외 : 성숙한 생태계, 타입스크립트 지원, Next.js 와 통합, 비동기 작업 관리
- 데이터 로딩, 에러처리 단순화
- 직관적인 로딩, 에러
✳️ 핵심기능
- 데이터 가져오기와 캐싱 : useQuery 함수 사용하여 데이터 불러오기, 자동캐싱, 중복요청 방지 가능
- 자동 리페치 : 데이터를 주기적으로 업데이트 해야하는 경우, refetchInterval 옵션 사용하여 설정 가능
- 뮤테이션 : useMutation 으로 데이터 변경작업 (생성,수정,삭제) 간단하게 수행 가능
✳️ 리액트쿼리 캐싱
데이터를 가져와서 처음 한번 캐싱하면, 이후 동일한 데이터에 대한 요청은 네트워크 요청을 보내지 않고 캐싱된 데이터를 사용한다.
- staleTime 옵션 : 캐시된 데이터의 “잘못된” 상태 (stale state) 를 얼마동안 허용할지 설정하는데 사용
- 잘못된 상태란 ? 데이터가 업데이트 되었지만 이전에 캐싱된 데이터가 여전히 사용 가능한 상태
- stateTime 의 기본값은 0. 데이터가 한 번 불러와지면 요청 시에는 항상 새로운 데이터를 가져온다.
const component = () => { const {data} = useQuery('myData', get('/mydata'),{ staleTime: 60000 , //60 sec }) }
useQuery : 데이터를 가져올 때 사용하는 함수
queryKey 라는 고유 식별 문자열 키를 기준으로 데이터를 캐싱하고 자동으로 리페칭 관리.
로딩, 에러, 데이터 등을 처리할 수 있는 옵션 제공
useQueryClient : 리액트쿼리 클라이언트에 접근해서 다양한 작업 수행 가능 (캐시조작,캐시데이터작업)
queryClient.invalidateQueries : 캐시된 데이터와 쿼리 간 상호작용이 가능. 쿼리를 다시 로드하여 갱신한다.
즉, 캐시된 데이터를 무효화 하고 최신화 된 데이터로 갱신하는 메소드이다.
useMutation : 데이터 변경 작업 수행에 사용
Next.js 와 함께 사용하기
SSR 은 getServerSideProps , SSG 는 getStaticProps 와 함께 리액트쿼리 사용 가능
리액트쿼리로 데이터를 미리 가져와 페이지를 서버에서 렌더링 가능
데이터를 사전에 로드하고 서버나 클라이언트에서 캐싱을 처리하여 성능을 최적화 시킬 수 있다.
const Popsts = (props) => { const {data} = useQuery({ queryKey:['posts'], queryFn : getPopsts, initialData : props.posts, //서버에서 가져온 데이터 }) } //서버에서 미리 렌더링 시키기 export const getStaticProps = async() =>{ const posts = await getPosts() return {props : {posts}} }
- axios 는 응답을 자동으로 JSON 형식으로 파싱해준다.
fetch 처럼 따로 json 으로 파싱하는 작업이 필요없다.
timeout, header 설정가능
요청 취소도 가능하여 component unmount 되었을때 컨트롤이 가능하다.
fetch
export async function getServerSideProps() { const stores = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/api/stores` ).then((res) => res.json()); return { props: { stores }, }; }
axios
export async function getServerSideProps() { const stores = await axios(`${process.env.NEXT_PUBLIC_API_URL}/api/stores`); return { props: { stores }, }; }
01_React Query 로 페이지네이션 구현하기
페이지네이션 설계하기
- 페이지넘버를 params 로 넘겨서 각 페이지에 맞는 데이터를 10개만 불러오도록 작업하기
- 총 페이지수가 10개 이하일 경우 각 페이지를 클릭할 수 있도록 아래와 같이 모든 페이지 숫자를 화면에 나열한다.
- 만약 총 페이지수가 10개 이상일 경우 현재 페이지 숫자만 화면에 표시하고 이전,다음 버튼으로 페이지를 이동한다.
✳️ Store 페이지
- router query 로 현재 페이지 값 받기
- /api/stores?page=${id} 형태
- 현재 페이지 숫자를 강조한 형태로 페이지네이션 컴포넌트 보여주기
✳️ 페이지네이션 컴포넌트
- 총 페이지 갯수 (totalPages) 와 현재 페이지(page) 매개변수로 받음
- totalPages 만큼 매핑을 하면서 페이지 이동링크를 만든다.
- query:{page:N}
- 현재 페이지는 파란색으로 표현
- 만약 총 페이지 수가 10보다 크면 이전,다음버튼 보여준다.
✳️ Prisma 로 Next.js API Routes 설계
- 현재 페이지를 변수로 받아서 해당 페이지 데이터만 가공해서 넘겨준다
prisma 의 skip과 take 활용
const results = await prisma.post.findMany({ skip: 3,//현재페이지 -1 : 첫번째페이지는 스킵을 하면 안되기 때문 take: 4, //몇개씩 보여줄지 결정 where: { email: { contains: 'prisma.io', }, }, })
02_React Query 로 무한스크롤 구현하기
react-query 의 InfiniteQueries 를 이용하여 무한스크롤 기능을 제공한다.
const {data,error, fetchNextPage,hasNextPage,isFetching,isFetchingNextPage,status} = useInfiniteQuery({ queryKey:['projects'], queryFn: fetchProjects, getNextPageParam : (lastPage,pages) => lastPage.nextCursor })
✳️ infiniteQuery 기능
fetchPosts : 페이지 별 데이터를 가져오는 역할
getNextPageParam : 콜백함수를 사용해서 다음페이지를 정의한다.
더보기버튼 클릭하여 무한스크롤 구현하기
const {data, fetchNextPage, hasNextPage} = useInfiniteQuery({ 'posts' ,//쿼리 고유의 키 fetchPosts, //데이터 가져오는 비동기 함수 { getNextPageParam : (lastPage,pages) => lastPage.nextCursor //다음페이지 파라미터 추출 } }) ... <div> {data.pages.map((page,pageIdx)=>{ <div key={pageIdx}>{page}</div> }) </div> {hasNextPage ? <button onClick = {()=>fetchNextPage()} disabled={isFetching} > {isFetching ? '로딩중':'더보기'} </button> : null }
응용코드
const fetchStore = async ({ pageParam = 1 }) => { const { data } = await axios("/api/stores?page=" + pageParam, { params: { limit: 10, page: pageParam, }, }); return data; }; const { data: stores, isFetching, fetchNextPage, isFetchingNextPage, hasNextPage, isLoading, isError, } = useInfiniteQuery("stores", fetchStore, { getNextPageParam: (lastPage: any) => lastPage.data?.length > 0 ? lastPage.page + 1 : undefined, }); const fetchNext = useCallback(async () => { const res = await fetchNextPage(); if (res.isError) { console.dir("ERROR=>", res.error); } }, [fetchNextPage]); useEffect(() => { let timerId: NodeJS.Timeout | undefined; if (isPageEnd && hasNextPage) { timerId = setTimeout(() => { fetchNext(); }, 500); } return () => clearTimeout(timerId); }, [fetchNext, fetchNextPage, hasNextPage, isPageEnd]); ... <button type="button" onClick={() => fetchNextPage()}> Next page </button>
더보기를 클릭하지 않고 자동으로 무한스크롤 하기
특정 영역에 도달했을때 가시성을 통해 특정 뷰포트에 도달했는지 확인할수 있는 기능인 intersection Observer 를 활용하면 된다.
- useIntersectionObserver 훅을 생성하여 리스트 하단 ref 에 도달했는지 (isIntersecting) 확인
- 만약 페이지 하단에 도달하고, 다음 페이지가 있다면 리액트쿼리의
fetchNextPage()
함수를 호출한다. - 페이지 하단에 도달했을 시 0.5초 뒤에 fetchNextPage() 함수 호출하고 그동안 로더를 띄워준다.
- 마지막 페이지에 다다를 때 까지 위 단계를 반복한다.
- 마지막 페이지에 도달하면 더이상 페이지 수가 올라가지 않도록 설계 (lastPage)
import useIntersectionObserver from '@/hooks/useIntersectionObserver' import {useInfiniteQuery} from 'react-query' const listRef = useRef<HTMLDivElement | null>(null); const listEnd = useIntersectionObserver(listRef,{}); const isEndPage = !!listEnd?.isIntersecting; useEffect(()=>{ if(isEndPage && hasNextPage) { fetchNextPage(); } },[fetchNextPage,hasNextPage,isEndPage])
✳️ infiniteQuery 주요 개념
- data : infinite Query 결과 데이터
- data.pages : 가져온 페이지들의 배열
- data.pageParams : 페이지를 가져오기 위한 페이지 매개 변수, 배열의 형태
- fetchNextPage, fetchPreviousPage : 다음 페이지 및 이전 페이지의 데이터를 가져오는 함수
- getNextPageParam, getPreviousPageParam : 다음 및 이전페이지에 대한 매개변수를 생성하는 함수
- hasNextPage, hasPreviousPage : 다음 페이지 및 이전페이지가 있는지 여부를 나타내는 불리언 값
- isFetchingNextPage , isFetchingPreviousPage : 다음페이지 또는 이전페이지의 데이터를 가져오는 동안 로딩상태를 나타내는 불리언 값