2025. 8. 26. 20:44ใReact
์ต์ข UI
์ด๋ฒคํธ ๋์ ์ ์ํ๊ธฐ
๋ฆฌ๋ทฐ ๋ฐ์ดํฐ๋ ํ ํ์ด์ง์ 20๊ฐ์ฉ ํ์๋๋ฉฐ, ์ฌ์ฉ์๊ฐ ํ๋ฉด์ ๋ง์ง๋ง ๋ฐ์ดํฐ์ ๋๋ฌํ๋ฉด ์๋์ผ๋ก ๋ค์ ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ ์ด์ด์ ํ์๋๋๋ก ๊ตฌํํ์ต๋๋ค.
์ด๋ฒคํธ ๋์ ๊ตฌํํ๊ธฐ
๐ ์ฐธ๊ณ ์๋ฃ
useInfiniteQuery๋ก ๋ฌดํ์คํฌ๋กค ๊ตฌํํ๊ธฐ | ์ฌ๋ฆฌ๋ธ์ ํ ํฌ๋ธ๋ก๊ทธ
๋ฌดํ์คํฌ๋กค ๊ตฌํ ๋ฐฉ๋ฒ๊ณผ ๋ค๋ก๊ฐ๊ธฐ ์ ์คํฌ๋กค ์ ์งํ๋ ๋ฐฉ๋ฒ์ ์๊ฐํฉ๋๋ค.
oliveyoung.tech
useInfiniteQuery | TanStack Query React Docs
tsx const { fetchNextPage, fetchPreviousPage, hasNextPage, hasPreviousPage, isFetchingNextPage, isFetchingPreviousPage, ...result } = useInfiniteQuery({ queryKey, queryFn: ({ pageParam = 1 }) = fetchP...
tanstack.com
[React] TanStack-Query์ useInfiniteQuery ํ ์ ์ด์ฉํ์ฌ ๋ฌดํ์คํฌ๋กค ๊ตฌํํ๊ธฐ
TanStack-Query ๋ ๋ค์ํ ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ฉฐ, ๊ทธ ์ค ๋ฌดํ์คํฌ๋กค ๊ธฐ๋ฅ๋ ์ ๊ณตํ๋ค. TanStack-Query์์ ์ ๊ณตํ๋ useInfinityQuery ํ ์ ์ฌ์ฉํ๋ฉด ์ฝ๊ฒ ๋ฌดํ์คํฌ๋กค ๊ธฐ๋ฅ์ ๊ตฌํํ ์ ์๋ค.์ด๋ฒ ๊ธ์์๋ TanStack-Q
junhee1203.tistory.com
React Query ์ ์ฉ์ ์ํ ๊ธฐ๋ณธ ์ค์
useInfiniteQuery์ ๊ฐ์ Query ๊ธฐ๋ฐ ํ ์ ์ฌ์ฉํ๊ธฐ ์ํด์๋ QueryClient ์์ฑ๊ณผ Provider ์ ์ฉ์ด ํ์ํฉ๋๋ค.
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient(); //๋ชจ๋ ์ฟผ๋ฆฌ์ ์บ์๋ฅผ ๊ด๋ฆฌํ๋ ์ค์ ์ ์ฅ์ ์ญํ ์ ํฉ๋๋ค.
์ฟผ๋ฆฌ ์บ์, ์ ์ญ ์ค์ , ์ฟผ๋ฆฌ ๋ฌดํจํ ๋ฑ ๋ค์ํ ๊ธฐ๋ฅ์ ํด๋น ๊ฐ์ฒด๋ฅผ ํตํด ๊ด๋ฆฌํ ์ ์์ต๋๋ค.
- React Query๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด ์ฑ ์ ์ฒด์ QueryClient๋ฅผ ๊ณต๊ธํด์ผ ํฉ๋๋ค.
- Provider ์์์๋ง useQuery, useInfiniteQuery ๊ฐ์ ํ ์ ์ฌ์ฉํ ์ ์์ต๋๋ค.
createRoot(document.getElementById('root')).render(
<QueryClientProvider client={queryClient}>
<StrictMode>
<App />
</StrictMode>,
</QueryClientProvider>
)
useInfiniteQuery๋ฅผ ํตํ ๋ฌดํ ์คํฌ๋กค ๊ตฌํ
useInfitieQuery์ฌ์ฉ์ ์ํด์๋ ์๋์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ค์นํด์ฃผ์ด์ผ ํฉ๋๋ค.
npm install @tanstack/react-query
useInfitieQuery์ ํ ๋นํ ๊ฐ์ฒด ๋ด์ ์ต์ ์ ์๋์ ๊ฐ์ต๋๋ค.
import { useInfiniteQuery } from "@tanstack/react-query";
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
status,
error,
} = useInfiniteQuery({
queryKey: ["reviews", clinicParam.id], //reviews์ id๊ฐ์ผ๋ก ์ฟผ๋ฆฌ์ ๊ณ ์ ์๋ณํค ์ค์
queryFn: ({ pageParam }) => fetchReview(pageParam, clinicParam), //๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ํจ์
initialPageParam: 0, //์ด๊ธฐ pageParam ์ค์
getNextPageParam: (lastPage) => {
const nextPage = lastPage.data.page.number + 1;
return nextPage <= lastPage.data.page.totalPages ? nextPage : undefined;
},
});
queryKey
ํน์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ์ฟผ๋ฆฌ ์์ฒด์ ๊ณ ์ ํ ์๋ณํค์ ๋๋ค.
queryKey: ["reviews", clinicParam.id] // ['reviews', 1] ํํ๋ก ์บ์์ ์ ์ฅ๋ฉ๋๋ค.
[์ญํ ]
์บ์ฑ
- ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ฉด queryKey ๋ฅผ ๊ธฐ์ค์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์บ์์ ์ ์ฅํฉ๋๋ค.
- ๋์ผํ queryKey ์์ฒญ ์, ์๋ก์ด ์์ฒญ ์์ด ์บ์ ๋ฐ์ดํฐ๋ฅผ ๋ฐํํฉ๋๋ค.
๋ฐ์ดํฐ ๋ฌดํจํ ๋ฐ ์ ๋ฐ์ดํธ
- ๋ฐ์ดํฐ๊ฐ ๋ณ๊ฒฝ๋ ๊ฒฝ์ฐ queryKey๋ฅผ ํ์ฉํด ์ด๋ค ์บ์๋ฅผ ๋ฌดํจํํ๊ณ ๋ค์ ๊ฐ์ ธ์ฌ์ง ๊ฒฐ์ ํฉ๋๋ค.
์ฟผ๋ฆฌ ์ธ์คํด์ค ์๋ณ
- ์ฌ๋ฌ ์ปดํฌ๋ํธ์์ ๊ฐ์ queryKey ์ฌ์ฉ ์, ํ๋์ ์ฟผ๋ฆฌ ์ธ์คํด์ค๋ก ์ธ์๋์ด ์ํ๊ฐ ์๋์ผ๋ก ๋๊ธฐํ ๋ฉ๋๋ค.
queryFn
queryFn์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ํจ์๋ฅผ ์ง์ ํฉ๋๋ค
queryFn: ({ pageParam }) => fetchReview(pageParam, clinicParam)
initialPageParam: 0 //์ด๊ธฐ pageParam์ ์ค์ ํฉ๋๋ค.
- queryFn์์๋ pageParam์ ๋งค๊ฐ๋ณ์๋ก ์ ๋ฌํฉ๋๋ค.
- ์ด๊ธฐ๊ฐ์ ์ค์ ํ์ง ์์ผ๋ฉด ๊ธฐ๋ณธ์ ์ผ๋ก undefined๊ฐ ๋ฉ๋๋ค.
fetchReviewํจ์
export function fetchReview(page, clinicParam) {
const token = localStorage.getItem("accessToken").trim();
return api.get(
`/clinics/${clinicParam.id}/reviews`,
{
params: {
"page": page
},
headers: {
"Authorization": `Bearer ${token}`
},
}
)
}
getNextPageParam
๋ค์ ํ์ด์ง๋ฅผ ์์ฒญํ ๋ ์ฌ์ฉํ pageParam ๊ฐ์ ๊ฒฐ์ ํ๋ ํจ์์ ๋๋ค.
lastPage: ๊ฐ์ฅ ๋ง์ง๋ง ์์ฒญ์ ํตํด ๋ฐ์์จ data์ ๋๋ค.
getNextPageParam: (lastPage) => {
... ์๋ต
}
๊ตฌํํ ์ฝ๋์์ ๋ฆฌ๋ทฐ ์์ฒญ ์, ์๋ต ๋ฐ์ดํฐ๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
"data": {
"content": [
{}
],
"page": {
"size": 20,
"number": 0, //ํ์ฌ ํ์ด์ง
"totalElements": 70,
"totalPages": 4 //์ด ํ์ด์ง
}
}
nextPage๋ ํ์ฌ ํ์ด์ง ๋ฒํธ + 1๋ก ๊ณ์ฐํฉ๋๋ค.
๋ง์ฝ, ๋ง์ง๋ง ํ์ด์ง๋ผ๋ฉด undefined๋ฅผ ๋ฐํํ์ฌ ์ถ๊ฐ ์์ฒญ์ด ๋ฐ์ํ์ง ์์ต๋๋ค.
getNextPageParam: (lastPage) => {
const nextPage = lastPage.data.page.number + 1
return nextPage <= lastPage.data.page.totalPages ? nextPage : undefined
}
์์ ์ต์
์ ํตํด ์์ฒญ์ด ์๋ฃ๋๋ฉด ์ต์ข
์ ์ผ๋ก ์๋์ ๊ฐ์ ๋ฐ์ดํฐ๋ค์ ์๋ตํฉ๋๋ค.
const {
data, // ์ฟผ๋ฆฌ ์๋ต ๋ฐ์ดํฐ
fetchNextPage, // ๋ค์ ํ์ด์ง ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ํจ์
hasNextPage, // ๋ค์ ํ์ด์ง ์กด์ฌ ์ฌ๋ถ
isFetchingNextPage, // ๋ค์ ํ์ด์ง ๋ก๋ฉ ์ํ
status, // ์ฟผ๋ฆฌ ์ํ
error, // ์๋ฌ ์ ๋ณด
} = useInfiniteQuery({
...์๋ต
});
useInView๋ฅผ ํ์ฉํ ํ๋ฉด ๊ฐ์ง ๊ธฐ๋ฐ ๋ฌดํ ์คํฌ๋กค
useInView ์ฌ์ฉ์ ์ํด์๋ ์๋์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ค์นํด์ผ ํฉ๋๋ค.
npm i react-intersection-observer
useInView๋ฅผ ์ด์ฉํ๋ฉด ํน์ DOM์ด ํ๋ฉด์ ๋ค์ด์๋์ง ๊ฐ์งํ ์ ์์ต๋๋ค.
ref
ํ๋ฉด์์ ๊ฐ์ง๋ฅผ ํ๊ณ ์ถ์ ์์๋ ์์น์ ์ง์ ์ฐ๊ฒฐํ๋ ์์ฑ์ ๋๋ค.
inView
์ฐ๊ฒฐ๋ ์์๊ฐ ํ๋ฉด ์์ ๋ค์ด์ ์๋์ง๋ฅผ ํ๋จํ์ฌ true ๋๋ false์ผ๋ก ๊ฐ์ ๊ฐ์ง๋ ์์ฑ์ ๋๋ค.
import { useInView } from "react-intersection-observer";
const { ref, inView } = useInView();
useEffect(() => {
if (inView && hasNextPage && !isFetchingNextPage) {
console.log("Fetching next page...")
fetchNextPage() //๋ชจ๋ ์กฐ๊ฑด์ด ๋ง์กฑํ๋ค๋ฉด ๋ค์ ํ์ด์ง์ ๋ฐ์ดํฐ๋ฅผ ์์ฒญํฉ๋๋ค.
}
}, [inView, hasNextPage, isFetchingNextPage, fetchNextPage])
์ด์ ์ ๋ฐ์์จ data๋ฅผ ํ์ฉํ์ฌ ReviewForm ์ปดํฌ๋ํธ๋ก ์ถ๋ ฅํ๊ณ , ๋ฌดํ ์คํฌ๋กค ๊ฐ์ง๋ฅผ ์ํ ref๋ฅผ ๋ง์ง๋ง์ ์ถ๊ฐํฉ๋๋ค.
const renderContent = () => {
switch (activeSection) {
...์๋ต
case "๋ฆฌ๋ทฐ":
return (
<div className="p-6 bg-gray-50 rounded-lg">
{data?.pages[0]?.data?.page?.totalElements === 0 ? (
<>
<h3 className="text-xl font-semibold mb-4">ํ์ ๋ฆฌ๋ทฐ</h3>
<p className="font-bold">๋ฆฌ๋ทฐ๊ฐ ์กด์ฌํ์ง ์์ต๋๋ค. </p>
</>
) : (
<>
<div className="flex flex-row">
<h3 className="text-xl font-semibold mb-4">ํ์ ๋ฆฌ๋ทฐ: {data?.pages[0]?.data?.page?.totalElements}</h3>
<button
className="text-white ml-auto bg-black px-4 rounded"
onClick={() => navigate("/review", {state: {clinicId: clinicParam.id}})}
> ๋ฆฌ๋ทฐ ์์ฑํ๊ธฐ</button>
</div>
<div className="space-y-4">
{data?.pages
?.flatMap((page) => page.data.content)
.map((review, index) => (
<ReviewForm
key={`${review.id || index}`}
author={review.author}
content={review.content}
imageURLs={review.imageURLs}
createdAt={review.createdAt}
rating={review.starPoint}
/>
))}
{/* ํ๋ฉด ๊ฐ์ง ์์น (๋ฌดํ ์คํฌ๋กค์ฉ) */}
{<div ref={ref} />}
</div>
</>
)}
</div>
)
...์๋ต
}
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import './App.css'
import App from './App.jsx'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; //์ถ๊ฐ
const queryClient = new QueryClient()//์ถ๊ฐ
createRoot(document.getElementById('root')).render(
<QueryClientProvider client={queryClient}> //์ถ๊ฐ
<StrictMode>
<App />
</StrictMode>,
</QueryClientProvider>
)
์ด๋ฒ ํ๋ก์ ํธ์์๋ useInfiniteQuery์ useInView๋ฅผ ํ์ฉํ์ฌ ์ฌ์ฉ์๊ฐ ์คํฌ๋กคํ ๋ ์๋์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ๋ถ๋ฌ์ค๋ ๋ฌดํ ์คํฌ๋กค์ ๊ตฌํํ์์ต๋๋ค.
์ด ๋ฐฉ์์ ํ์ฉํ๋ฉด ๋ฒํผ ํด๋ฆญ ์์ด๋ ์์ฐ์ค๋ฝ๊ฒ ๋ฆฌ๋ทฐ ๋ฐ์ดํฐ๋ฅผ ์ด์ด์ ๋ณด์ฌ์ค ์ ์์ผ๋ฉฐ ์ฟผ๋ฆฌ ์บ์ ๊ด๋ฆฌ์ ๋ฐ์ดํฐ ๋ฌดํจํ๋ ๊ฐํธํ๊ฒ ์ฒ๋ฆฌํ ์ ์์ต๋๋ค.