♾️ 무한 스크롤링 ( + 스로틀링 )

박상은·2022년 5월 6일
0

✏️ blelog ✏️

목록 보기
9/13

결과적으로 useSWRInfinite()throttle을 이용해서 구현했습니다.

👇 무한 스크롤링 구현

velog의 메인 페이지를 보면 일정 구간 이하로 스크롤을 내릴 때마다 게시글을 추가로 가져오게 됩니다.
해당 기능을 useSWRInfinite(), throttle, 스크롤 이벤트를 이용해서 구현했습니다.

infinite-scroll

💁 useSWRInfinite

이전에 댓글 더 불러오기를 구현할 때 설명과 예시를 작성한 적이 있기 때문에 이번 포스팅에서는 실제 적용 코드만 첨부하겠습니다.

// /api/posts?page=[page]&offset=[offset]&kinds=[kinds] 형태로 요청
const { data: responsePosts, setSize } = useSWRInfinite<ApiResponseOfPosts>(
  (pageIndex, previousPageData) => {
    if (previousPageData && previousPageData.posts.length !== offset) {
      setHasMorePost(false);
      return null;
    }
    if (previousPageData && !previousPageData.posts.length) {
      setHasMorePost(false);
      return null;
    }
    return `/api/posts?page=${pageIndex}&offset=${offset}&kinds=popular`;
  }
);

🤨 useInfiniteScroll 훅

현재 코드에서 사용한 방식은 throttle 방식입니다.
그 이유는 debounce를 적용하면 계속 스크롤을 할 경우 요청이 계속 지연되는 문제가 발생하기 때문입니다.

1. throttle 적용 예시

throttle이란 한번 요청하면 일정 시간 동안 해당 요청을 무시하는 것을 말합니다.
아래 코드에서는 setTimeOut()을 이용해 첫 요청 이후 400ms 이후에 새로운 요청을 받도록 구현했습니다.

import { useCallback, useEffect, useState } from "react";

type Props = {
  condition: boolean;
  // useSWRInfinite()가 반환하는 setSize()
  setSize: (size: number | ((_size: number) => number)) => any;
};

const useInfiniteScroll = ({ condition, setSize }: Props) => {
  const [throttle, setThrottle] = useState(false);

  // 2022/05/06 - 인피니티 스크롤링 함수 - by 1-blue
  const infiniteScrollEvent = useCallback(() => {
    if (!condition) return;
    if (
      window.scrollY + document.documentElement.clientHeight >=
      document.documentElement.scrollHeight - 400
    ) {
      if (throttle) return;

      setThrottle(true);
      setTimeout(() => {
        setThrottle(false);
        setSize((prev) => prev + 1);
      }, 400);
    }
  }, [condition, setSize, throttle, setThrottle]);

  // 2022/05/06 - 무한 스크롤링 이벤트 등록/해제 - by 1-blue
  useEffect(() => {
    window.addEventListener("scroll", infiniteScrollEvent);

    return () => window.removeEventListener("scroll", infiniteScrollEvent);
  }, [infiniteScrollEvent]);
};

export default useInfiniteScroll;

2. debounce 적용 예시

debounce란 연속적으로 들어온 요청 중에 가장 마지막 요청만 실행하는 것을 의미합니다.
아래 코드는 setTimeout()을 이용해서 400ms 이내에 연속적으로 요청이 들어오면 clearTimeout()을 이용해서 타이머를 해제하고 다시 등록하는 방식으로 동작합니다.

import { useCallback, useEffect, useState, useRef } from "react";

type Props = {
  condition: boolean;
  setSize: (size: number | ((_size: number) => number)) => any;
};

// 2022/05/06 - useSWRInfinite() + 무한 스크롤링을 적용하는 훅 - by 1-blue
const useInfiniteScroll = ({ condition, setSize }: Props) => {
  // 2022/05/06 - 타이머 아이디 - by 1-blue
  const timerId = useRef<any>(null)

  // 2022/05/06 - 인피니티 스크롤링 함수 - by 1-blue
  const infiniteScrollEvent = useCallback(() => {
    if (!condition) return;
    if (
      window.scrollY + document.documentElement.clientHeight >=
      document.documentElement.scrollHeight - 400
    ) {
      if (timerId.current) clearTimeout(timerId.current);
      timerId.current = setTimeout(() => setSize((prev) => prev + 1), 400);
    }
  }, [condition, setSize, throttle, setThrottle]);

  // 2022/05/06 - 무한 스크롤링 이벤트 등록/해제 - by 1-blue
  useEffect(() => {
    window.addEventListener("scroll", infiniteScrollEvent);

    return () => window.removeEventListener("scroll", infiniteScrollEvent);
  }, [infiniteScrollEvent]);
};

export default useInfiniteScroll;

🥲 발생한 경고

1. 레이아웃과 key 문제

  • 현재 상황
    1. useSWRInfinite()로 인해 이차원배열 형태로 데이터가 들어온다.
    2. 반응형을 적용해서 한 라인에 게시글이 2~4개까지 배치된다. ( grid )
  • 최초 적용한 방법
// 2차원 배열이므로 Array.prototype.map()을 두 번 사용해서 배치
// 발생한 문제: <ul> 집합끼리만 grid가 적용돼서 중간에 게시글이 빈 부분이 생김
{responsePosts?.map(({ posts }, index) => (
  <ul
    key={index}
    className="grid gird-col-1 gap-x-8 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"
  >
    <>
      {posts.map((post, i) => (
        <Post
          key={post.id}
          post={post}
          photoSize="w-full h-[300px]"
          $priority={i < 4}
        />
      ))}
    </>
  </ul>
))}
  • 수정한 방법
// <ul>안에 <Post />를 렌더링해서 배치는 정상적으로 작동함
// 하지만 responsePosts?.map()에서 key를 사용하는 부분이 없기 때문에 경고가 발생함
<ul className="grid gird-col-1 gap-x-8 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
  {responsePosts?.map(({ posts }) => (
    <>
      {posts.map((post, i) => (
        <Post
          key={post.id}
          post={post}
          photoSize="w-full h-[300px]"
          $priority={i < 4}
        />
      ))}
    </>
  ))}
</ul>
  • 해결한 방법
// <Post />만 넣는 배열을 따로 만들어서 렌더링함
// 배치도 정상적이고, key에 대한 경고도 사라짐
const [list, setList] = useState<any>([]);
useEffect(() => {
  setList(
    responsePosts?.map(({ posts }) =>
      posts.map((post, i) => (
        <Post
          key={post.id}
          post={post}
          photoSize="w-full h-[300px]"
          $priority={i < 4}
        />
      ))
    )
  );
}, [responsePosts]);

// jsx
<ul className="grid gird-col-1 gap-x-8 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
  {list}
</ul>

2. 스크롤 이벤트 문제

스크롤 이벤트는 초당 수십번에서 수백 번까지 발동해서 그런지 throttle이나 denouncedebounce를 적용해도 가끔 무시하고 여러 번 요청되는 경우가 발생합니다.

이 문제는 제가 코드를 잘못 짜서 그런 건지 스크롤 이벤트가 너무 단기간에 많이 발동해서인지 정확한 원인은 파악하지 못했고 어떻게 해결할지도 찾지 못해서 일단 기록하고 넘어가겠습니다.

🐲 참고 링크

0개의 댓글