포스팅 목차
1. 구현 완료 후 영상
* 해당 영상은 "네트워크 > 느린 4G" 환경에서 찍은 영상입니다.(데이터를 불러올 때의 UI도 보여드리기 위함)
2. 필요한 라이브러리 정리
"@tanstack/react-query": "^5.59.16",
설치 방법
npm i @tanstack/react-query
tanstack/react-query 공식 문서 : https://tanstack.com/query/latest/docs/framework/react/installation
3. 코드와 함께 상세 설명
1. Request&Response 값 타입 정의
backend에서 받아오는 response 값과 request 값의 타입은 아래와 같습니다. (해당 코드에서는 필요한 값들만 정의했습니다.)
interface TContents {
board_id: number;
title: string;
}
// 응답 값 타입 정의
interface GetBoardListResponse {
status: "SUCCESS" | "FAIL";
message: string;
body: {
contents: TContents[];
page: number;
hasNext: boolean;
};
}
// 요청 값 타입 정의
interface GetBoardListRequest {
page: number;
pageSize: number;
}
2. backend에 요청을 보낼 api 정의
data : 백엔드에게 요청해서 받아온 응답 값
fetchNextPage: 다음 페이지 값을 가져오도록 하는 함수.
isFetchingNextPage: fetchNextPage로 다음 페이지를 가져오는 동안 true 반환.
queryKey: query key를 통해 데이터를 캐싱함.
queryFn: Promise를 반환하는 임의의 함수. Promise는 데이터나 에러를 반환한다. useInfiniteQuery에서는 getNextPageParam에서 반환한 값(pageParam)을 인수로 받는다.
getNextPageParam: query에 대한 새로운 데이터가 수신될 때 호출되며, 리스트의 마지막 페이지와 모든 페이지의 정보를 인수로 받는다. 해당 함수에서는 queryFn에 전달할 하나의 변수를 반환해야 한다. 또한, 다음 페이지가 없음을 나타내려면 undefined을 반환해야 한다.
initialPageParam: 초기 pageParam값 정의.
select: queryFn에서 반환한 data값을 가공하여 반환할때 사용함.
const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery({
queryKey: ["data"],
queryFn: async ({ pageParam }) => {
const params: GetBoardListRequest = { page: pageParam, pageSize: 10 };
const res = await axios.get<GetBoardListResponse>(
"http://localhost:8080/board/pagination",
{ params }
);
if (res.data.status === "SUCCESS") {
return res.data.body;
}
throw new Error(res.data.message || "board 불러오기 실패");
},
getNextPageParam: (data) => {
if (data) {
return data.hasNext ? data.page + 1 : undefined;
}
return undefined;
},
initialPageParam: 0,
select: (data) => {
const contentPage = data.pages.flatMap((aPage) => aPage.contents);
return {
content: contentPage,
pageParams: data.pageParams,
};
},
});
3. 다음 페이지를 불러오기 위해 교차점으로 활용할 target 설정
<div ref={targetRef} /> : fetchNextPage()함수를 불러올 교차점. 리스트 맨 아래 위치하게 하여, 사용자 화면에서 해당 div박스가 보일 때를 '교차했다' 라고합니다.
intersectionObserver.observe(targetRef.current) : 관찰할 대상을 지정합니다.
new IntersectionObserver : 관찰 대상은 entries배열에 들어가 있으며, 우리의 관찰 대상은 하나뿐이므로 바로 entries[0]을 관찰하면 됩니다(우리가 관찰하는 대상 = entries[0]). 그러므로, 관찰대상을 교차했을 경우 entries[0].isIntersecting = true가 되므로 fetchNextPage()함수 실행되여 다음 페이지의 데이터를 불러옵니다.
const App = () => {
const targetRef = React.useRef<HTMLDivElement>(null);
{/* api 요청하는 useInfiniteQuery 위에서 설명하였으므로 생략 */}
React.useEffect(() => {
// 관찰자
const intersectionObserver = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) fetchNextPage();
});
if (targetRef && targetRef.current) {
intersectionObserver.observe(targetRef.current);
}
return () => {
if (targetRef.current) {
intersectionObserver.unobserve(targetRef.current);
}
};
}, [targetRef, fetchNextPage]);
return (
<div>
{*/ ... 리스트들 불러오는 코드 생략*/}
{isFetchingNextPage && (
<div
style={{
{*/ ... style 생략*/}
}}
>
<h1>Loading..</h1>
</div>
)}
<div ref={targetRef} />
</div>
);
};
IntersectionObserver 에 대해서 : https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver
4. 전체 코드 및 주석
import "./App.css";
import axios from "axios";
import { useInfiniteQuery } from "@tanstack/react-query";
import React from "react";
interface TContents {
board_id: number;
title: string;
}
// 응답 값 타입 정의
interface GetBoardListResponse {
status: "SUCCESS" | "FAIL";
message: string;
body: {
contents: TContents[];
page: number;
hasNext: boolean;
};
}
// 요청 값 타입 정의
interface GetBoardListRequest {
page: number;
pageSize: number;
}
const App = () => {
const targetRef = React.useRef<HTMLDivElement>(null);
//react-query api 함수
const { data, fetchNextPage, isFetchingNextPage } = useInfiniteQuery({
queryKey: ["data"],
queryFn: async ({ pageParam }) => {
const params: GetBoardListRequest = { page: pageParam, pageSize: 10 };
const res = await axios.get<GetBoardListResponse>(
"http://localhost:8080/board/pagination",
{ params }
);
if (res.data.status === "SUCCESS") {
return res.data.body;
}
throw new Error(res.data.message || "board 불러오기 실패");
},
getNextPageParam: (data) => {
if (data) {
return data.hasNext ? data.page + 1 : undefined;
}
return undefined;
},
initialPageParam: 0,
select: (data) => {
const contentPage = data.pages.flatMap((aPage) => aPage.contents);
return {
content: contentPage,
pageParams: data.pageParams,
};
},
});
React.useEffect(() => {
// 관찰자
const intersectionObserver = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) fetchNextPage();
});
if (targetRef && targetRef.current) {
intersectionObserver.observe(targetRef.current);
}
return () => {
if (targetRef.current) {
intersectionObserver.unobserve(targetRef.current);
}
};
}, [targetRef, fetchNextPage]);
return (
<div>
<ul>
{data?.content.map((page) => (
<li
key={page.board_id}
style={{
marginBottom: "20px",
borderBottom: `1px solid black`,
listStyle: "none",
}}
>
<p>게시판 id :{page.board_id}</p>
<p>title: {page.title}</p>
</li>
))}
<div />
</ul>
{/* 해당 div 박스가 보일 때, 즉 스크롤 제일 하단일 때,
IntersectionObserver를 통해 해당 요소에 접근했다는 것을 알림.
해당 요소에 접근 했을 경우 entries[0].isIntersecting = True*/}
{isFetchingNextPage && (
<div
style={{
background: "black",
color: "white",
height: "200px",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<h1>Loading..</h1>
</div>
)}
<div ref={targetRef} />
</div>
);
};
export default App;
해당 코드에서는 api를 작성하는 로직과 화면을 구현하는 로직을 한 페이지에서 작성하였지만,
일반적으로 api코드와 화면 구현 코드는 분리해서 작업하는게 코드 유지보수에 더 좋습니다!
'웹 > (React)리액트' 카테고리의 다른 글
상태관리 라이브러리, Redux 에 대해서/Redux Toolkit에 대해서 (2) | 2023.09.15 |
---|---|
(React)리액트 클래스에 조건문/리액트에서 클래스 다양한 방법으로 사용해보기/클래스명 여러개/className 여러개 일때/클래스에 변수 사용하기 (20) | 2023.07.11 |
(React) 리액트 드롭다운 메뉴 만들기/마우스 오버시 드롭다운 - 1탄 (2) | 2022.12.17 |
(React with Typescript) module not found 'sass' 에러 해결, scss 사용법 (0) | 2022.10.27 |
(React) 리액트 커스텀 훅 custom hook 사용법+input 초기화 까지 - input (0) | 2022.09.24 |
댓글