Notice
Recent Posts
Recent Comments
Link
«   2026/07   »
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
Tags
more
Archives
Today
Total
관리 메뉴

mimizae 님의 블로그

페이지네이션에 대해서 본문

FE

페이지네이션에 대해서

mimizae 2025. 12. 21. 18:43

 

페이지네이션이 무엇이고, 왜 사용하며 어떻게 대량 데이터를 나누고 언제 다음 데이터를 요청하는지 알아보자!


페이지네이션이란?

은 양의 데이터를 화면에 한 번에 보여주지 않고, 여러 개의 페이지로 나누어 표시하는 기술이다!

 

페이지네이션은 왜 필요할까? 🤔

 

웹이나 앱에서 다루는 데이터의 양은 생각보다 빠르게 많아진다.

게시글 목록, 상품 리스트, 사용자 기록처럼 수백, 수천 건의 데이터를 한 화면에 모두 보여주려 한다면 여러 문제가 발생한다.

 

먼저, 성능 문제가 있다!

엄청나게 많은 데이터를 한 번에 받아오면 네트워크 비용이 증가하고, 데이터 로딩 속도도 느려진다. 이는 곧 사용자에게 긴 로딩 시간과 버벅거리는 화면으로 이어질 것이다.

 

또한, UX 측면에서도 분명히 좋지 않다!!한 화면에 너무 많은 정보가 노출되면 사용자는 한 번에 원하는 정보를 찾기 어려워지고, 스크롤이 끝없이 길어지면서 피로감을 느낀다.

 

이러한 문제들을 해결하기 위해 등장한 개념이 바로 페이지네이션(Pagination)이다!

 

그렇다면 많은 양의 데이터를 다뤄야 할 때, 데이터를 페이지 단위로 어떻게 나누고 언제 다음 데이터를 호출해야 하는 걸까?

 

페이지네이션은 다음  가지 관점에서 살펴볼 수 있다.

 

1. 데이터를 어떻게 나누는가 (offset, cursor)
2. 언제 다음 데이터를 요청하는가 (로딩 트리거가 무엇인지)
3. 받아 온 데이터들을 어떻게 관리하는가? (useQuery, useInfiniteQuery)

데이터를 어떻게 나누는가

받아온 데이터를 나누는 방식에는 두 가지가 있다. offset 방식cursor 방식이다.

 

offset 방식

offset 방식은 전체 데이터 중에서 특정 위치(offset)부터 일정 개수(limit)만큼의 데이터를 가져오는 방식이다.
주로 page와 size, 혹은 offset과 limit 값을 사용해 구현되는데, ORM이나 데이터베이스 쿼리에서는 skip과 take라는 용어를 사용한다!

 

skip(또는 offset)은 앞의 데이터를 몇 개 건너뛸지를 의미하고, take(또는 limit)는 실제로 조회할 데이터의 개수를 의미한다.

예를 들어, 한 페이지에 3개의 데이터를 보여준다고 가정하면

  • 1페이지: offset 0, limit 3
  • 2페이지: offset 3, limit 3
  • 3페이지: offset 6, limit 3

과 같은 방식으로 데이터를 요청한다!!

 


이 방식은 구현이 간단하고 직관적이며, 사용자가 몇 페이지에 있는지 명확하게 알 수 있다는 장점이 있다.
그래서 페이지의 이동이 중요할 때, 즉

  1. 페이지 번호가 중요한 경우에 (게시판, 검색 결과, 관리자 페이지 등)
  2. 특정 페이지로 바로 이동해야 할 때!

자주 사용된다!!

 

offset 방식의 단점

하지만 이 방식에는 치명적인 단점이 있다...!!! 😵

바로 데이터 중복, 누락의 문제가 있다. 먼저 데이터 중복에 대해 살펴보자면,

 

현재 offset 3, limit 3인 상황일 때, 서버에서 새로운 데이터가 추가되었다고 가정해 보자. 물론!! 새로운 데이터의 삽입은 서버에서 이루어지니 클라이언트는 이를 알 수 없으니 2.5라는 새로운 데이터가 삽입되었지만, 여전히 화면에 표시되는 데이터는 1, 2, 3이다.

 

이때 클라이언트에서 다음 페이지로 페이지네이션을 한다면?!!!!!!

클라이언트에서는 다음 페이지를 서버에 요청하면 서버는 당연히 새로 추가된 2.5를 포함해 개수를 세어 다음 데이터인 3, 4, 5를 보내줄 것이다.

그런데 위에서 말했듯, 클라이언트는 서버의 2.5 데이터 추가를 모르고 여전히 화면에 보여지는 것은 1, 2, 3 이었으니

다음 페이지에도 3이 중복으로 보이게 되는 것!!!

 

또 다른 문제점은, 데이터 삭제 시의 데이터 누락 문제이다... 😵

 

이번에는 아까 offset 3, limit 3 상태에서 3번 데이터가 삭제된다고 가정해 보자.

 

이때 3번 데이터가 삭제되었으니 서버에서는 1, 2, 4번까지가 3개의 offset으로 남게 되고! page 2에 대해서 클라이언트가 쿼리를 요청한다면 5번 데이터부터 불러와진다. 

따라서 page 1에서도 못 봤던 4번 데이터는 page 2에서도 못 보게 된다...

 

그래서 데이터가 매우 많거나, 실시간으로 데이터가 계속 추가 및 삭제되는 경우(무한 스크롤 UI)에는 사용에 부적합하다!

 

cursor 방식

 

가장 최근에 가져온 데이터를 기준으로 다음 데이터를 가져오는 방식의 Pagenation이다.

 

요청을 보낼 때, 가장 마지막 데이터의 id와 함께 몇 개의 데이터를 가져올 지 개수를 명시한다.

스크롤 형태의 리스트에서 자주 사용하며, 주로 어플리케이션에서 많이 사용하게 되는 형태이며 무한 스크롤 방식이 바로 커서 페이지네이션 방식이다!!!!!

 

Keyset pagination, 혹은 Token Based Pagination이라고도 한다.

 

페이지 기반 페이지네이션에 비해서 비교적 구현이 복잡하지만 최근 데이터의 id 값(또는 다른 식별자)을 기준으로 동작하기 때문에 데이터가 중복되거나 누락될 확률이 낮아진다!!!!! (offset 기반 방식의 단점을 보완 🤩)

 

이때 이 식별자를 커서(Cursor)라고 한다.

 

모바일에서는 거의 대부분의 앱이 페이지 기반 페이지네이션보다는 커서 기반 페이지네이션을 사용하고 있다.

커서 기반 페이지네이션은 사용성 측면에서 데이터가 연속적으로 불러와지다 보니, UX 측면에서 끊김없는 연속된 경험을 제공할 수 있다.

 

특히 사용자가 앱에 오래 남아있어야 하는 SNS, 유튜브 등은 사용자의 경험이 끊어진다는 느낌이 들지 않도록, 연속된 스크롤 뷰 형태의 UI를 제공하는 경우가 대부분이다!!

 

 

이제 동작 원리를 예시와 함께 살펴보자!!

가장 처음에 3개의 데이터를 요청하면 클라이언트는 서버로부터 들어온 데이터 중 가장 마지막 아이템의 id를 기억한다.

 

그리고 다음 요청을 보낼 때, 클라이언트는 마지막 아이템인 id가 3 이후의 데이터를 받아야 하니,

서버에게 마지막 id가 3이다! 라는 정보와 함께 추가로 3개의 데이터를 더 가져와 달라고 서버에 요청한다.

이 과정을 계속해서 반복한다!

 

여기서 잠깐... 만약에 🤔

더보기
만약, 클라이언트가 id를 보내는 게 아니라, 서버가 클라이언트에게 전송한 데이터의 마지막 id를 기억하고, 새롭게 추가 데이터 요청이 들어올 시 그 id를 기준으로 추가 데이터를 보내는 형식으로 서버를 구현한다면 어떨까?

 

이는 클라이언트 입장에서는 편할 수 있지만, 서버 측 구현이 매우 복잡해진다.

서버의 입장에서 각 클라이언트의 상태를 기억!!!!!해야 하고, 이는 곧 수평적인 확장이 어려워짐을 의미한다. 서버는 최대한 stateless를 지향해야 하고, 최대한 passive하게 설계되어야 한다. 

 

 

새로운 데이터 추가 시의, offset 기반 방식의 문제점이었던 데이터 누락 문제가 커서 기반에서도 일어날까??

 

커서 기반 페이지네이션은 마지막 아이템의 id를 기반으로 하기에, 신규 데이터가 추가된다 하더라도 데이터의 중복 문제 없이 페이지네이션이 진행된다!!

이때, 신규 데이터가 누락되는 것 자체는 어쩔 수 없다.
논리적 설계 측면에서도, 사용자가 관심있는 데이터는 id > 3인 데이터이다. 페이지를 이동했다는 것은 '다음' 데이터를 보고 싶다는 뜻이니! id=2.5인 데이터는 현재의 관심사가 아니다. 이는 새로고침 등을 통해서 신규 데이터를 확보하는 게 자연스럽다.

 

 

 

그럼 데이터 삭제 시의 데이터 누락 문제는 어떨까?

이때도 마찬가지로 id를 기반으로 하는 커서 기반 페이지네이션에서는 데이터 누락 문제 없이 페이지네이션이 동작한다!!!

 

3 데이터가 삭제되었다고 한들, 커서의 기준인 id > 3은 변하지 않기 때문에!!!! 누락 없이 3 이후의 데이터 요청이 가능한 것이다.

 

정리하자면...

페이지네이션은 크게 offset 기반과 cursor 기반으로 나뉜다.

 

Offset 기반 페이지네이션

  • offset / limit (skip / take)을 기준으로 데이터를 조회한다.
  • 구현이 단순하고 페이지 번호가 명확해 특정 페이지로 이동이 필요한 경우에 적합하다.
  • 하지만 데이터가 실시간으로 추가 및 삭제될 경우 데이터 중복이나 누락 문제가 발생할 수 있으며, 데이터가 많아질수록 성능 저하가 발생한다.

Cursor 기반 페이지네이션

  • 마지막으로 조회한 데이터의 id와 같은 식별자를 기준으로 다음 데이터를 조회한다.
  • 데이터 추가/삭제에도 커서의 기준이 흔들리지 않아 중복 및 누락 문제가 거의 발생하지 않는다.
  • 페이지 번호 이동은 어렵지만, 무한 스크롤 UI와 대용량 데이터 처리에 적합하다. 

따라서 페이지 이동이 중요한 게시판·검색·관리자 화면에는 offset 기반, 연속적인 데이터 소비가 중요한 SNS·피드·모바일 앱에는 cursor 기반 페이지네이션이 주로 사용된다!!!!


언제 다음 데이터를 요청하는가 (로딩 트리거)

다음 데이터를 언제 요청하는지는 로딩 트리거에 따라 달라진다.

로딩 트리거는 사용자의 행동이나 화면 상태를 기준으로 나눌 수 있다!

  • 버튼 클릭을 통해 데이터를 요청하는 방식 (페이지네이션 버튼)
  • 스크롤 이벤트를 감지해 데이터를 요청하는 방식 
  • Intersection Observer를 이용해 뷰포트 진입 시 데이터를 요청하는 방식
이 중에서 스크롤 이벤트와 Intersection Observer에 대해 집중적으로 설명하고자 한다.
페이지네이션 버튼 방식은 사용자가 명시적으로 페이지를 선택하는 구조이기 때문에 로딩 트리거 관점에서는 설명할 내용이 비교적 단순하다. 반면!! 스크롤 이벤트와 Intersection Observer 방식은 언제 다음 데이터를 요청할 것인가에 대한 구현 전략이 필요하기 때문이다!!

 

스크롤 이벤트 기반 구현

 

사용자의 스크롤 위치문서의 전체 높이비교해 페이지 하단에 도달했는지 확인하는 방식이다.

스크롤 이벤트만 사용해 구현하는 것은 정말 단순하게!! 스크롤이 발생할 때마다 즉시 이벤트 핸들러가 실행된다.

구현이 간단하지만, 성능 측면에서는 비효율적일 수 있는 방법이라고 볼 수 있다. 😵

 

const useScrollInfiniteRaw = (fetchNextPage, hasNextPage, isFetchingNextPage, threshold = 0.8) => {
  const [isNearBottom, setIsNearBottom] = useState(false);

  useEffect(() => {
    const handleScroll = () => {
      const scrollTop = window.scrollY || document.documentElement.scrollTop;
      const scrollHeight = document.documentElement.scrollHeight;
      const clientHeight = document.documentElement.clientHeight;

      // 스크롤이 하단에 가까워졌는지 확인
      const isNearBottom = (scrollTop + clientHeight) / scrollHeight > threshold;

      setIsNearBottom(isNearBottom);

      if (isNearBottom && hasNextPage && !isFetchingNextPage) {
        fetchNextPage();
      }
    };

    window.addEventListener("scroll", handleScroll);
    return () => window.removeEventListener("scroll", handleScroll);
  }, [fetchNextPage, hasNextPage, isFetchingNextPage, threshold]);

  return { isNearBottom };
};

 

위의 코드는 스크롤 이벤트 기반 로딩을 가장 직관적으로 구현한 예시이다!!
별도의 최적화 없이 모든 스크롤 이벤트를 그대로 처리하며, 스크롤이 발생할 때마다 현재 스크롤 위치를 계산해 페이지 하단에 가까워졌는지를 판단한다.

 

이 훅은 사용자가 스크롤할 때마다 handleScroll 함수가 연속적으로 호출되고, 그때마다 scrollTop, scrollHeight, clientHeight와 같은 레이아웃 관련 속성을 반복적으로 읽는다. 또한 스크롤 위치가 하단에 근접했는지 여부와 관계없이, 매 스크롤 이벤트마다 setIsNearBottom이 호출되어 상태 업데이트가 발생한다. (불필요한 리렌더링 발생!!)

 

즉... 여기에서 가장 큰 문제는 스크롤 이벤트가 매우 빈번하게 발생한다는 점이다...!!

사용자가 스크롤을 하는 동안 이벤트는 초당 수십에서 많게는 수백 번까지 호출될 수 있으며, 매 이벤트마다 현재 스크롤 위치를 계산하는 로직이 실행된다. 그렇다면 스크롤마다 레이아웃 관련 속성을 반복적으로 읽고, 불필요한 리렌더링이 반복 된다.

 

반복적으로 레이아웃 관련 값을 읽는 과정이 문제가 되는 이유는!!

더보기

브라우저로 하여금 최신 레이아웃 정보를 보장하기 위해 리플로우(reflow)를 발생시킬 수 있기 때문이다.

 

리플로우는 레이아웃 변경이나 측정이 일어날 때 발생하는데, DOM 요소의 추가/제거, 크기/위치 변경, 창 크기 조절 등이 트리거가 될 수 있다!!
즉... scrollTop, offsetHeight, getBoundingClientRect()과 같은 레이아웃 관련 속성 참조 시 브라우저는 최신 값을 반환하기 위해 리플로우를 수행한다.

 

리플로우가 발생하면 영향받는 요소 뿐 아니라 그 자식 요소, 때로는 전체 문서에 영향을 미치며 연속적인 리플로우는 프레임 드롭, 지터링, UI 응답성 저하 등 서비스에 악영향을 줄 수 있다...!!!!!

이런 리플로우가 스크롤 이벤트에서 레이아웃 속성을 반복적으로 읽을 때 빈번하게 발생하니 성능 문제의 원인이 될 수 있는 것이다.

 

이처럼 스크롤 이벤트를 직접 감지하는 방식은 구현 자체는 단순하지만, 별도의 제어 로직 없이 사용할 경우 불필요한 연산이 반복적으로 발생하고 성능 저하로 이어질 수 있다. 🥲

 

 

Intersection Observer 기반 구현

 

IntersectionObserver API는 특정 요소가 뷰포트(Viewport) 또는 지정한 루트 요소와 교차(intersection)하는지를 비동기적으로 감지하는 브라우저 API이다.

즉, 사용자가 얼마나 스크롤했는지를 직접 계산하는 대신, 관찰 대상 요소가 화면에 보이는 순간을 기준으로 이벤트를 발생시킨다.

 

 

이 API의 가장 큰!!! 특징은 스크롤 이벤트를 직접 구독하지 않는다는 점이다.

스크롤 이벤트 방식이 사용자 스크롤 동작마다 핸들러를 실행하는 반면, Intersection Observer는 브라우저 내부에서 요소의 가시성 변화를 관찰하다가 조건을 만족하는 시점에만 콜백을 호출한다. 이로 인해 불필요한 이벤트 호출을 크게 줄일 수 있다.

 

또한 Intersection Observer는 메인 스레드에서 스크롤 이벤트를 지속적으로 처리하지 않는다.

브라우저가 내부적으로 최적화된 방식으로 관찰을 수행하고, 레이아웃 계산이 필요한 경우에도 이를 묶어서 처리하기 때문에 메인 스레드의 부하를 최소화할 수 있다.

이러한 특성 덕분에 스크롤 이벤트 기반 구현에서 문제로 지적되었던 잦은 이벤트 호출과 불필요한 연산을 효과적으로 피할 수 있다!!! 🤩

 

useEffect(() => {
  const observer = new IntersectionObserver(
    (entries) => {
      if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
        fetchNextPage();
      }
    },
    { threshold: 0.3, rootMargin: "100px" }
  );

  if (observerTarget.current) {
    observer.observe(observerTarget.current);
  }

  return () => observer.disconnect();
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);

 

위의 코드에서는 IntersectionObserver의 옵션을 통해 관찰 조건을 제어한다!

  • threshold: 0.3
    관찰 대상 요소의 30% 이상이 뷰포트에 노출되었을 때 콜백이 실행된다. 이를 통해 사용자가 요소를 어느 정도 인지했을 때 데이터를 미리 로딩할 수 있다.
  • rootMargin: "100px"
    실제 뷰포트보다 상하로 100px 확장된 영역을 기준으로 관찰한다. 이를 통해 사용자가 실제로 요소를 보기 전에 미리 다음 데이터를 요청하는 선제적 로딩이 가능하다.

콜백 함수에서는 entries[0].isIntersecting 값을 통해 관찰 대상 요소가 현재 뷰포트에 진입했는지를 확인하고, 추가로 hasNextPage와 isFetchingNextPage 조건을 함께 사용해 중복 요청을 방지한다.


받아 온 데이터를 어떻게 관리하는가

이제 TanStack Query 얘기를 할 차례다!!

 

앞서 스크롤 이벤트나 Intersection Observer와 같은 로딩 트리거를 통해 언제 다음 데이터를 요청할지를 결정했다면, 그 다음으로는 요청을 통해 받아온 데이터를 어떻게 관리할 것인지가 중요해진다!!

 

무한 스크롤이나 더 보기 버튼과 같은 UI에서는 단순히 데이터를 한 번 가져오는 것으로 끝나지 않는다.

이전에 받아온 데이터는 유지한 채, 새로운 데이터를 계속해서 이어 붙여 보여줘야 하며, 이 과정에서 로딩 상태, 중복 요청, 에러 처리 같은 다양한 문제가 함께 발생한다.

 

이러한 문제를 모두 직접 구현할 수도 있지만, TanStack Query는 이러한 반복적인 패턴을 useInfiniteQuery라는 훅으로 추상화해 제공한다!!!!

 

useInfiniteQuery란 무엇인가... 🤔

 

useInfiniteQuery는 여러 페이지에 걸쳐 데이터를 요청하는 상황에서, 각 요청의 결과를 하나의 연속된 데이터 흐름으로 관리하기 위한 훅이다.


여기에서 중요한 점은, useInfiniteQuery가 무한 스크롤을 감지하지는 않는다는 것이다...!!! 대신 무한 스크롤이나 더 보기 버튼과 같은 UI에서 발생하는 데이터 관리 문제를 전담한다.

 

즉, 로딩 트리거(스크롤 이벤트, Intersection Observer, 버튼 클릭)는 여전히 UI 레벨에서 결정하고, 그 결과로 호출되는 데이터 요청 이후의 흐름을 useInfiniteQuery가 담당하는 구조다!!!!

(이건 스스로 헷갈렸던 지점이라 추가해 본다... 하핫 😅)

 

다음은  useInfiniteQuery의 가장 기본적인 형태이다! 

const {
  data,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage,
  isLoading,
  isError,
} = useInfiniteQuery({
  queryKey: ["posts"],
  queryFn: async ({ pageParam = 1 }) => {
    const res = await fetch(`/api/posts?page=${pageParam}`);
    return res.json();
  },
  getNextPageParam: (lastPage) => {
    return lastPage.nextPage ?? undefined;
  },
});

 

이 코드에서 핵심은 다음과 같다.

  • queryFn은 페이지 단위 데이터를 가져오는 함수이며, pageParam을 통해 다음 요청에 필요한 값을 전달받는다.
  • getNextPageParam은 다음 페이지가 존재하는지를 판단하는 역할을 하며, 반환값이 undefined가 되면 hasNextPage는 false가 된다.
  • fetchNextPage는 다음 페이지 데이터를 요청하기 위한 함수로, 로딩 트리거가 발생했을 때 호출된다.
data = {
  pages: [
    { posts: [...] }, // 첫 번째 페이지
    { posts: [...] }, // 두 번째 페이지
    { posts: [...] }, // 세 번째 페이지
  ],
  pageParams: [1, 2, 3],
};

 

useInfiniteQuery는 각 페이지 요청의 결과를 data.pages라는 배열로 누적 관리한다.

 

이 구조 덕분에 개발자는 이전 데이터를 직접 합치거나 관리할 필요 없이, 다음과 같이 전체 데이터를 렌더링할 수 있다!!!

 

const posts = data?.pages.flatMap(page => page.posts);

 

이처럼 useInfiniteQuery는 이전 데이터를 유지하고, 새로운 데이터를 자동으로 이어 붙이며, 중복 요청을 방지하기 위한 상태(isFetchingNextPage)까지 함께 제공한다.


Intersection Observer와 useInfiniteQuery의 결합

그럼 위에서 살펴본 로딩 트리거 중 하나인 Intersection ObserveruseInfiniteQuery를 결합해, 실제로 무한 스크롤이 어떻게 구현되는지를 전체적으로 살펴보자!!!

export const useIntersectionPagination = () => {
  const ref = useRef<HTMLDivElement | null>(null);

  const query = useInfiniteQuery({
    queryKey: ["movies-intersection"],
    initialPageParam: 1,
    queryFn: ({ pageParam = 1 }) => fetchPopularMovies(pageParam),
    getNextPageParam: (lastPage) =>
      lastPage.page < lastPage.total_pages ? lastPage.page + 1 : undefined,
  });

  useEffect(() => {
    if (!ref.current) return;

    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting && query.hasNextPage) {
          query.fetchNextPage();
        }
      },
      { threshold: 1 }
    );

    observer.observe(ref.current);
    return () => observer.disconnect();
  }, [query.hasNextPage]);

  return {
    movies: query.data?.pages.flatMap((p) => p.results) ?? [],
    ref,
    isLoading: query.isLoading,
  };
};

 

먼저 useInfiniteQuery는 페이지네이션된 영화 데이터를 관리하는 역할을 담당한다.

  • queryFn은 pageParam을 받아 해당 페이지의 데이터를 요청한다.
  • getNextPageParam은 마지막 페이지 정보를 기준으로 다음 페이지가 존재하는지 판단한다.
  • 더 이상 요청할 페이지가 없을 경우 undefined를 반환해 자동으로 요청을 중단한다.
  • 각 페이지의 응답은 data.pages 배열에 순서대로 누적된다.

이 과정에서 개발자는 이전 데이터를 직접 합치거나 상태를 관리할 필요가 없고, useInfiniteQuery가 이를 내부적으로 처리해준다.

 

Intersection Observer는 화면 하단에 위치한 특정 요소(ref)가 뷰포트에 완전히 진입했을 때를 감지하는 역할을 한다.

const ref = useRef<HTMLDivElement | null>(null);

 

이 ref는 보통 리스트의 맨 아래에 위치한 sentinel 요소에 연결된다.

사용자가 스크롤을 내려 이 요소가 화면에 보이게 되면, Intersection Observer의 콜백 함수가 실행된다.

 

if (entries[0].isIntersecting && query.hasNextPage) {
  query.fetchNextPage();
}

 

이 시점에서 Intersection Observer는 이제 다음 데이터를 요청해도 되는 시점이다!라는 신호만을 전달하고, 실제 데이터 요청은 useInfiniteQuery가 제공하는 fetchNextPage를 통해 수행된다.


참고 자료

React-Query useInfiniteQuery사용해 무한스크롤 기능 구현하기

useInfiniteQuery로 무한스크롤 구현하기

useInfiniteQuery (TanStack Query 공식 사이트)

Scroll event부터 Intersection Observer API까지 

실무에서 느낀 점을 곁들인 Intersection Observer API 정리

Intersection Observer API

페이지네이션 offset과 cursor 차이

페이지네이션

 

'FE' 카테고리의 다른 글

에러 핸들링을 하는 방법(Suspense와 ErrorBoundary)  (0) 2026.01.02
CI/CD란 뭘까?  (0) 2025.12.26
Tanstack Query의 데이터 최신화  (0) 2025.12.14
낙관적 업데이트 적용하기  (0) 2025.12.05
스크롤 감지 Tab-Bar 구현하기  (0) 2025.11.26