useInfiniteQuery โž• useInView๋กœ ๋ฌดํ•œ ์Šคํฌ๋กค ๊ตฌํ˜„ํ•˜๊ธฐ

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๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž๊ฐ€ ์Šคํฌ๋กคํ•  ๋•Œ ์ž๋™์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋ฌดํ•œ ์Šคํฌ๋กค์„ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค. 

์ด ๋ฐฉ์‹์„ ํ™œ์šฉํ•˜๋ฉด ๋ฒ„ํŠผ ํด๋ฆญ ์—†์ด๋„ ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋ฆฌ๋ทฐ ๋ฐ์ดํ„ฐ๋ฅผ ์ด์–ด์„œ ๋ณด์—ฌ์ค„ ์ˆ˜ ์žˆ์œผ๋ฉฐ ์ฟผ๋ฆฌ ์บ์‹œ ๊ด€๋ฆฌ์™€ ๋ฐ์ดํ„ฐ ๋ฌดํšจํ™”๋„ ๊ฐ„ํŽธํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.