일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- 컴퓨터 네트워크
- react
- 프로그래밍 언어론
- 백준
- 장고
- 리액트 훅
- vanillaJS
- websocket
- useState
- react firebase
- 프로그래머스 자바
- 디자인 패턴
- React JS
- 데이터모델링과마이닝
- 코딩테스트 고득점 Kit 완전탐색
- 자바스크립트
- codesandbox
- Java
- 리액트
- JavaScript
- 자바
- 프로그래머스 완전탐색
- 프로그래머스
- 자바 공부
- design pattern
- useEffect
- react hook
- NextJS
- 코딩테스트 고득점 Kit
- 코틀린
- Today
- Total
기록하는 개발자
[NextJs] React-Query의 useInfiniteQuery로 무한스크롤 구현하기 본문
nextjs+typescript+tailwindcss 환경에서 영화 api를 사용한 토이 프로젝트를 진행중이다.
(영화 api https://developer.themoviedb.org/reference/intro/getting-started)
https://mingmeng030.tistory.com/270
https://mingmeng030.tistory.com/275
프로젝트의 nav bar에서는 키워드를 통해 영화 검색을 할 수 있는데,
아래와 같은 검색 결과 화면을 무한스크롤을 통해 보여주기 위해
react-query의 useInfiniteQuery를 사용해보기로 했다.
1. react-query 설치
npm i axios react-query
2. _app.js 수정
import { QueryClient, QueryClientProvider } from "react-query";
import { ReactQueryDevtools } from "react-query/devtools";
import "../styles/globals.css";
import Layout from "../components/Layout";
export default function App({ Component, pageProps }) {
const queryClient = new QueryClient();
return (
// QueryClientProvider로 인해 모든 페이지 및 컴포넌트에서 queryClient에 접근 가능
<QueryClientProvider client={queryClient}>
<Layout>
<Component {...pageProps} />
</Layout>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
3. 무한스크롤을 사용할 파일
- 아래와 같이 import 후 시작하였다.
import { useInfiniteQuery } from "react-query";
import { useRef } from "react";
import axios from "axios";
(1) api 호출 함수 작성
//호출할 api 함수로 작성
const fetchMovies = ({ pageParam = 1 }: type.fetchMovieProps) =>
axios
.get(`${config.api}/api/search/${keywordToShow}/${pageParam}`)
.then((res) => {
return res;
});
api 호출 결과 response
- data 객체 내 현재 페이지 번호(page) , 영화 리스트(results), 전체 페이지 수(total_pages), 전체 결과 수(total_results)가 있다.
(2) useInfiniteQuery 작성
const {
data, // api 함수 호출 결과 response와 pages, pageParams가 담긴 배열
fetchNextPage, // 다음 페이지를 불러오는 함수
status // loading, error, success 중 하나의 상태, string
} = useInfiniteQuery(
["movielist"], // data 이름(queryKey)
fetchMovies, // fetch callback : 작성한 api 호출 함수
{
getNextPageParam: (lastPage) => {
const page = lastPage.data.page;
if (lastPage.data.total_pages == page) return false;
return page + 1;
},
}
);
data
pages
fetch callback을 통해 return된 page 객체의 배열을 갖고 있다.
pageParams
useInfiniteQuery가 현재 어떤 페이지에 있는지를 확인할 수 있는 파라미터 값
getNextPageParam
getNextPageParam: (lastPage) => {
const page = lastPage.data.page; // 현재 페이지
// 현재 페이지와 전체 페이지수가 동일하면 false return
// -> 더 이상 fetch api 실행 안하고 무한 스크롤 종료
if (lastPage.data.total_pages == page) return false;
return page + 1;
},
getNextPageParam 함수가 false 를 return하면 반환하면 api 호출이 더 이상 실행되지 않는다.(무한스크롤 종료)
false를 return하지 않는 경우에는 Number를 리턴해야 하고, 다음 page 를 return 하도록 한다.
(Number를 return 하는 경우 자동으로 fetch callback의 인자로 pageParam을 전달한다.)
내가 호출하는 api의 res 구조는 data안에 page라는 parameter가 있기 때문에
lastPage.data.page로 접근하였다. (lastPage : 이전 페이지 객체)
현재 페이지가 전체 페이지수와 같으면 false를 return하고 그렇지 않은 경우 현재 page+1을 return 한다.
(3) IntersectionObserver
사용자가 스크롤을 맨 밑으로 내렸을 때 브라우저가 이를 감지하고 자동으로 fetchNextPage 함수를 작동 시킨다면 무한 스크롤이 가능해질 것이다.
IntersectionObserver는 접촉이 감지되었을 때 실행할 callback 함수와 접촉을 감지할 조건을 option으로 가진다.
const oberserver = new IntersectionObserver(([entry]) => onIntersect() , { ...option } )
useObserver.js
import { useEffect } from "react";
export const useObserver = ({
target, // 감지할 대상(여기서는 ref를 전달했다.)
root = null, // 교차할 부모 요소(default : document)
rootMargin = "0px", // root와 target이 감지하는 여백의 거리
threshold = 1.0, // 임계점으로, 1.0이면 root내에서 target이 100% 보여질 때 callback이 실행된다.
onIntersect, // target 감지 시 실행할 callback 함수
}) => {
useEffect(() => {
let observer;
// 넘어오는 element가 있어야 observer를 생성
if (target && target.current) {
// callback의 인자로 들어오는 entry는 기본적으로 순환자이기 때문에
// 복잡한 로직을 필요로 할때가 많다.
// callback을 선언하는 곳에서 로직을 짜서 통째로 넘기도록 하겠다.
observer = new IntersectionObserver(onIntersect, {
root,
rootMargin,
threshold,
});
// 실제 Element가 들어있는 current 관측 시작
observer.observe(target.current);
}
// observer를 사용하는 컴포넌트가 해제되면 observer도 끈다
return () => observer && observer.disconnect();
}, [target, rootMargin, threshold]);
};
useObserver 호출 부분
onIntersect
useObserver로 넘겨줄 callback 이다.
entry로 넘어오는 HTMLElement가 isIntersecting이라면 무한 스크롤을 위한 fetchNextPage가 실행된다.
const onIntersect = ([entry]) => entry.isIntersecting && fetchNextPage();
useObserver({
target: bottom,
onIntersect,
});
전체 코드
import { useInfiniteQuery } from "react-query";
import { useObserver } from "./useObsever";
import { useRef } from "react";
import axios from "axios";
import { useRouter } from "next/router";
import { config } from "../../static/config";
import Seo from "../../components/Seo";
import styles from "../../styles/Search.module.css";
import Link from "next/link";
import * as type from "./types";
import * as commonType from "../../types/commonType";
export default function searchResult() {
const router = useRouter();
const regex = /[\s\{\}\[\]\/?.,;:|\)*~`!^\-_+<>@\#$%&\\\=\(\'\"]+/g;
const keywordToShow = router.query.params[0].replace(/[+]/g, " ");
// 화면의 바닥 ref를 위한 useRef
const bottom = useRef(null);
const fetchMovies = ({ pageParam = 1 }: type.fetchMovieProps) =>
axios
.get(`${config.api}/api/search/${keywordToShow}/${pageParam}`)
.then((res) => {
console.log(res);
return res;
});
const { data, fetchNextPage, status, refetch } = useInfiniteQuery(
["movielist"],
fetchMovies,
{
getNextPageParam: (lastPage) => {
const page = lastPage.data.page;
if (lastPage.data.total_pages == page) return false;
return page + 1;
},
}
);
const onIntersect = ([entry]) => entry.isIntersecting && fetchNextPage();
useObserver({
target: bottom,
onIntersect,
});
return (
<div className="margincenter w-4/5">
<Seo title={"search result"}></Seo>
{status === "loading" && <p>불러오는 중</p>}
{status === "error" && <p>불러오기 실패</p>}
{status === "success" && data && (
<>
<p>"{keywordToShow}" 검색 결과 입니다.</p>
<p>총 {data.pages[0].data.total_results}개의 검색 결과가 있습니다.</p>
<div className="flexwrap">
{data.pages?.map((page) => {
const movieList: commonType.apiResult[] = page.data.results;
return movieList.map((movie) => {
return (
<Link>...</Link>
);
});
})}
</div>
</>
)}
// 바닥 ref를 위한 div 생성
<div ref={bottom} />
</div>
);
}
결과 화면
참고
'Web > NextJs' 카테고리의 다른 글
[NextJs] TailwindCss component (1) | 2023.05.11 |
---|---|
[NextJs] NextJs에 TailwindCss 적용 하기 (1) | 2023.05.11 |
[NextJs] NextJs에 Swiper 적용하기 (1) | 2023.05.11 |
[NextJs] getStaticProps와 getServerSideProps (0) | 2023.05.09 |
[NextJs] next.config.js 의 redirects와 rewrites (0) | 2023.05.09 |