Tanstack Query(구 React-Query)에서 제공하는 무한 스크롤을 쉽게 구현하게
도와주는 useInfiniteQuery를 괘니 그냥 특별한 이유없이 심심하게 해보장.
[꼭 써야 하는 건 아니당. 안 쓰고도 얼마든지 구현할 수 있당. 아무튼 그러탕!]
그라믄 또 이제는 손이 지겨워할 반복 준비 작업부터 시작하장.
(기본 세팅된 폴더를 가지고 있고, 그걸 복사해서 쓰고 있다면 당신은 이미 캡짱이당!)
1. 이름 영어로 아무렇게나 어딘가 나중에도 찾기 쉬운 곳에 새 폴더를 만들공
2. vscode로 해당 폴더를 연 후, 터미널 열공
3. npx create-vite@latest . 명령으로 필요한 파일들 현재 폴더에 다운로드
4. npm i 명령으로 package.json 파일에 기술된 모듈들 설치
5. 필요 없는 파일들(App.css 등등..) 정리하고
6. npm i @tanstack/react-query 명령으로 tanstack-query 모듈 설치
7. npm i react-intersection-observer 명령으로 intersection-api 모듈 설치
8. npm i tailwindcss @tailwindcss/vite 명령으로 tailwindcss 모듈 설치
[설정은 너무도 쉬우닝, 알아서 할 수 있을거란 믿음에 의심을 살짝 풀칠한당]
9. npm run dev
10. 영문자 O 누르고 엔터 눌러 브라우져 열기
7번에 설치한 react-intersection-observer 모듈은 IntersectionAPI를 추상화해서
바로 거저 편하게 쓸 수 있도록 한 것으로 궁금증을 참을 수 없다면 아래 글을 읽장
2024.02.14 - [자바스크립트] - IntersectionAPI 오디 한번 싸용해 보올깡
Tanstack Query를 사용하기 위한 설정 먼저 하장.
(툭하면 Provider당, 이젠 전혀 낯설지가 않아용! 사랑해도 될까용? 노래?)
main.jsx
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import ReactDOM from "react-dom/client"; import App from "./App"; import "./index.css"; const queryClient = new QueryClient(); ReactDOM.createRoot(document.getElementById("root")).render( <QueryClientProvider client={queryClient}> <App /> </QueryClientProvider> );
비록 예쁘게 꾸밀 재주는 없지만, 꾸믈 꾼당. 그저 칼라풀에서라도 허우적거리는
index.css
@import "tailwindcss"; .e7ecenter { @apply flex flex-col justify-center items-center; } .e7espin { @apply animate-spin w-15 h-15 rounded-[50%] border-5 border-t-blue-400 border-r-transparent border-b-pink-300 border-l-transparent border-amber-400; } .e7etitle { @apply mx-auto rounded-2xl max-w-250 text-7xl text-center mt-3 mb-5 bg-emerald-700 text-yellow-100; } .e7eouter { @apply flex flex-col gap-5 border-20 p-5 border-amber-600 border-t-transparent border-l-transparent overflow-scroll mx-auto max-w-250 h-[750px]; } .e7eitem { @apply rounded-2xl p-4 text-3xl text-white text-center; } .e7eend { @apply rounded-4xl text-center bg-violet-800 text-white text-3xl; }
로딩중을 알려주는 뺑뺑이도 미리 1개 맹글장(index.css랑 연합작전이당)
Spinner.jsx
function Spinner() { return ( <div className="e7ecenter"> <div className="e7ecenter e7espin">E7E</div> </div> ); } export default Spinner;
가짜 데이터도 미리 필요하당. (src 아래 api라고 폴더 만들고, 그 안에 만들장)
[왜냐하면 서버에 요청해서 데이터를 받은 것처럼 나 자신을 속이기 위함이당)
friends.js
// 초기 가짜 데이터 const ranColor = () => { return `rgb(${Math.floor(Math.random() * 255)},${Math.floor( Math.random() * 255 )},${Math.floor(Math.random() * 255)})`; }; let bgcolor = "blueviolet"; const friends = [ { id: 1, name: "추우니", color: ranColor(), bgcolor, }, { id: 2, name: "영시니", color: ranColor(), bgcolor, }, { id: 3, name: "세이니", color: ranColor(), bgcolor, }, { id: 4, name: "지혀니", color: ranColor(), bgcolor, }, { id: 5, name: "사나니", color: ranColor(), bgcolor, }, { id: 6, name: "E7E니", color: ranColor(), bgcolor, }, { id: 7, name: "경미니", color: ranColor(), bgcolor, }, { id: 8, name: "선주니", bgcolor, }, { id: 9, name: "꼬시니", color: ranColor(), bgcolor, }, ]; Array.from({ length: 77 }).map((_, i) => { if (!(i % 9)) bgcolor = ranColor(); friends.push({ id: 10 + i, name: `누구니 ${i + 10}`, color: ranColor(), bgcolor, }); }); //console.log("체로롱 초기 데이터", items.slice(0, 10)); const PCOUNT = 9; export function fetchFriends({ pageParam }) { console.log("디버깅 pageParam ", pageParam); return new Promise((resolve) => { setTimeout(() => { resolve({ data: friends.slice(pageParam, pageParam + PCOUNT), currentPage: pageParam, nextPage: pageParam + PCOUNT < friends.length ? pageParam + PCOUNT : null, }); }, Math.round(Math.random() * 1000) + 1000); }); }
PCOUNT =9 는 한 페이지당 9개씩 출력하겠다는 내 뜻이다.(당신 뜨슨?)
fetchFriends 함수에서 구조분해로 받는 매개변수 pageParam이 누네 낯설수 있당.
누늘 크게 뜨고, 계산식을 요래조래 보면 보이는데, 안보이면 console.log 활용
처음엔 0이 온당. 그래서 0 부터 8까지 slice해 간당 (1 페이지, 9개).
두번째는 9가 오고 그래서 9부터 17까지 slice해 간당(2 페이지, 9개).
세번째는 18...... 머 이따구의 반복인데, Promise와 setTimeout 을 이용하여
랜덤하게 1~2초 딜레이를 두어서 비동기 Fake를 만들었당. (오켕?)
이제 다 되었당. useInfiniteQuery를 머리에 써보자.
App.jsx
import { useInfiniteQuery } from "@tanstack/react-query"; import { useEffect, useRef } from "react"; import { useInView } from "react-intersection-observer"; import { fetchFriends } from "./api/friends"; import Spinner from "./Spinner"; export default function App() { const outRef = useRef(null); const { data, error, status, fetchNextPage, isFetchingNextPage } = useInfiniteQuery({ queryKey: ["items"], queryFn: fetchFriends, initialPageParam: 0, getNextPageParam: (lastPage) => lastPage.nextPage, }); console.log("디버깅 체킁1", isFetchingNextPage, status); // intersection api const { ref, inView } = useInView(); /* 여기는 그냥 재미로 넣은 부분 if (outRef.current) { setTimeout(() => { console.log("디버깅 체킁2", data.pages[data.pages.length - 1].nextPage); outRef.current.scrollTo(0, outRef.current.scrollHeight); let check = data.pages[data.pages.length - 1].nextPage; if (!check) { setTimeout(() => { location.reload(); }, 3000); } }, 500); } */ useEffect(() => { if (inView) { fetchNextPage(); } }, [fetchNextPage, inView]); return ( <div className="mt-5"> <h1 className="e7etitle">Query 무한스크롤!</h1> {status === "pending" ? ( <Spinner /> ) : status === "error" ? ( <div>{error.message}</div> ) : ( <div className="e7eouter" ref={outRef}> {data.pages.map((page) => { return ( <div key={page.currentPage} className="flex flex-col gap-1"> <h1 className="text-center text-6xl"> 쭈니 {Math.ceil(page.currentPage / 9) + 1} 페이징 </h1> {page.data.map((friend) => { return ( <div key={friend.id} style={{ backgroundColor: `${friend.bgcolor}`, color: `${friend.color}`, fontWeight: "bolder", }} className="e7eitem" > {friend.name} </div> ); })} </div> ); })} <div className="h-10" ref={ref}> {isFetchingNextPage ? ( <Spinner /> ) : ( <h1 className="e7eend">이젠 끝인가 보오! 메롱?</h1> )} </div> </div> )} </div> ); }
useInfiniteQuery 훅은 편하게 쓰라공, 많은 걸 돌려준다.
{ data, error, status, fetchNextPage, isFetchingNextPage } 를 가지고 입맛에 맞게
요리해 쓰면 좋을 것이다. 옵션 설정 중, 아래 설정이 필요한데,
fetchFriends 함수에 pageParam을 넘겨서, 다음 페이지 데이터를 가져오게 된당.
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextPage,
isFetchingNextPage을 이용하여 데이터를 가져오고 있는 중이면 뺑뺑이를 돌렸당
useInView() 훅이 intersection API를 편하게 쓸 수 있게 만든다.
ref를 원하는 곳에 설정하면, 설정된 애가 화면에서 안보이다가 스크롤바를 움직여 시야에
들어오게 되면 inView가 true가 된당.
(실제 intersection API는 얼매나 보이는지 %로 상세히도 알려준당)
const { ref, inView } = useInView();
주의 할게 하나 있는데(개인적 의견) useInView 훅의 ref를 설정한 div가 height가
없어서 생겼는데도 인식하지 못한 상황이 발생했었다. 내용이 없더라도 height는 주장
실행 화면은 아래와 같당.
(역시 멋진 쭈니의 걷는 모습은 보는 사람에게 에너지를 나눠준당. 질투가 벌써 하늘이다)
예쁘진 않지만 다행이당. 기능은 동작한당.
App.jsx 파일 안에 /* 여기는 그냥 재미로 넣은 부분 ...생략 */ 이 있당.
주석을 풀고 한번 실행해 보면 스스로 스크롤을 해서 자동으로 그걸 할꺼이다.
사실 무한 스크롤 기능은 가져와야 할 데이터가 많다면
백엔드와의 다양한 전략 궁합을 구사하는 센스있는 센스가 필요하당.
브라우져의 localStorage나 indexedDB도 조커가 될 수 있다.
가끔 Spring에서 무한스크롤용 데이터를 Thread로 처리하는 이야기도 들었는데.
구체적 상황을 알아야 명확하겠지만, 톰캣은 접속자별로 Thread로 동작하므로
접속자 1명인 상황으로 인식을 스스로 가두고 테스트 결과를 믿는 건....
웹의 성능을 따지려 한다면 항상 동시 접속자수를 머리에 담아보장.~~
Less is More
구차하고 상세하고 배려깊은 설명이 부족함을 알지만,
당신에겐 이미 그런 사소한 설명이 필요없음도 안당.
나보다 복사/붙여넣기가 빠른 AI가 당신을 도울거시다.
우리가 살아가는 이유, 아마도 원하는 게 있어서당.
내가 살아가는 이유, 나도 분명 원하는 게 있을게당.
단지 그게 먼지, 우리가 모르고, 내가 모를 뿐
설마 나만 모르는 건가?. 난 우리 밖에 있는 건강?
그래서 난 오늘도 먼지처럼 뿌연 하루를 맞는다.
내가 원하는 거 그게 멀까?
진심 찾아보기는 했던가?,
그저 세상이 깔아놓은 그 길 위에 날 얹었던 건가?
과거 지금처럼 뛰면 안되는 꾸부정 허리 아닌
막 뛰어도 문제 없던 튼트니 허리와 무릎과 함께한 그 시절
지금 내가 원하는 건 바로 그 시절의 나다
원하는 건 있는 데, 얻을 수 없음도 안다.
내가 살아가는 이유는 벌써 오래전에 거짓이다.
살아가는 진심이유를 마트에 사러가는 나
품절이면 어떡하징!!!!
https://www.youtube.com/watch?v=okVTSehE414
React(리액트) 티키타카 30 (SweetAlert2) (0) | 2025.03.24 |
---|---|
React(리액트) 티키타카 29 (CkEditor) (0) | 2025.03.22 |
React(리액트) 티키타카 27 (AG-Grid 활용 2) (0) | 2025.03.17 |
React(리액트) 티키타카 26 (bootstrap 아니면 tailwindcss) (0) | 2025.03.14 |
React(리액트) 티키타카 25 (AG-Grid 활용 1) (0) | 2025.03.14 |