리액트 무한 스크롤링(React infinite scroll)

박지성 학부생·2024년 1월 28일
1

FrontEnd Develop log

목록 보기
2/12

벨로그 클론코딩을 하면서 벨로그의 메인페이지가 페이지 네이션이 아닌 필요할때 데이터를 가져오는 무한 스크롤링 인것을 봐버렸다.

이미 봐버린 이상 해야한다...

무한 스크롤링: instagram이나 youtube shorts 같은 경우 화면을 계~속 밑으로 내릴때마다 데이터를 실시간으로 가져오는 그런 기능

벨로그 홈페이지를 보면 데이터를 한번에 가져오는 것이 아닌 화면 스크롤 시 실시간으로 가져옴

준비물

Card데이터, react-intersection-observer 끝

  • 우선 나는 React와 타입스크립트를 사용하였다.

  • npm install react-intersection-observer --legacy-peer-deps
    
  • 이제 react-intersection-observer에서 제공하는 useInView를 사용할 수 있다.

useInView는 웹 페이지에서 특정 부분이 화면에 보이는지를 감지하는 기능을 제공한다.
이를 사용하면 사용자가 해당 부분을 볼 때만 특정 작업을 실행할 수 있다. => 성능을 향상
사용자가 스크롤해서 페이지의 맨 아래에 도달했을 때 추가 콘텐츠를 로드하는 무한 스크롤 기능을 쉽게 구현할 수 있다.

import React, { useEffect, useState, useCallback, FC } from "react";
import CardTemplate from "../templates/CardTemplate";
import { useInView } from "react-intersection-observer";
import styled, { keyframes } from 'styled-components';
import { CardData as InitialCardData } from "../../assets/data/CardData";
import { Card } from "../../state/atoms/cardState";
import { useRecoilState } from "recoil";
import { tabPanelState } from "../../state/atoms/tabPanelState";
import theme from "../../styles/theme";

// 페이지당 로드할 카드의 수를 정의 여기 예시에서는 가져오는 과정을 천천히 보이기 위해 하나씩 가져오는걸로 설정
const pageSize = 1;

// Cards 컴포넌트 정의
const Cards: FC = () => {
  // 카드 데이터, 현재 페이지, 더 로드할 데이터가 있는지 여부를 상태로 관리
  const [cards, setCards] = useState<Card[]>([]);
  const [page, setPage] = useState<number>(1);
  const [hasMore, setHasMore] = useState<boolean>(false);

  // 'react-intersection-observer'를 사용하여 무한 스크롤 구현을 위한 ref와 inView 상태를 정의함
  const [ref, inView] = useInView({ threshold: 1 });

  // Recoil 상태 관리 라이브러리를 사용하여 선택된 탭의 상태를 가져옵니다.
  const [selectedTab] = useRecoilState(tabPanelState);

  // 선택된 탭에 따라 카드를 정렬하는 함수입니다.
  const sortCards = useCallback(() => {
    let sortedCards = [...InitialCardData];
    if (selectedTab === "1") {
      sortedCards.sort((a, b) => b.likeCount - a.likeCount);
    } else if (selectedTab === "2") {
      sortedCards.sort(
        (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()
      );
    }
    return sortedCards;
  }, [selectedTab]);

  // 선택된 탭이 변경될 때마다 카드를 새로 정렬하고 첫 페이지를 로드하는 useEffect 훅입니다.
  useEffect(() => {
    const sortedCards = sortCards();
    setCards(sortedCards.slice(0, pageSize));
    setPage(1);
    setHasMore(sortedCards.length > pageSize);
  }, [selectedTab, sortCards]);

  // 추가 카드를 로드하는 함수입니다. 현재 페이지 다음의 카드를 로드합니다.
  const loadMoreCards = useCallback(() => {
    if (hasMore) {
      const nextPage = page + 1;
      const sortedCards = sortCards();
      const nextCards = sortedCards.slice(page * pageSize, nextPage * pageSize);
      setCards((prevCards) => [...prevCards, ...nextCards]);
      setPage(nextPage);

      // 더 이상 로드할 카드가 없으면 hasMore를 false로 설정합니다.
      if (nextPage * pageSize >= sortedCards.length) {
        setHasMore(false);
      }
    }
  }, [hasMore, page, sortCards]);

  // 스크롤이 아래쪽에 도달하면 추가 카드를 로드합니다.
  useEffect(() => {
    if (inView && hasMore) {
      loadMoreCards();
    }
  }, [inView, loadMoreCards, hasMore]);

  // 카드를 그리드 형태로 표시하고, 더 로드할 카드가 있으면 로딩 스피너를 표시합니다.
  return (
    <CardGrid>
      {cards.map((card: Card) => (
        <CardTemplate key={card.id} card={card} />
      ))}
      {hasMore && <LoadingSpinner ref={ref} />}
    </CardGrid>
  );
};

export default Cards;

const CardGrid = styled.div`
  display: grid;
  justify-content: space-between;
  align-content: space-between;
  grid-gap: 45px;
  grid-template-columns: repeat(5, 1fr);
  overflow-y: auto;

  @media (max-width: 1600px) {
    grid-template-columns: repeat(4, 1fr);
  }
  @media (max-width: 1200px) {
    grid-template-columns: repeat(3, 1fr);
  }
  @media (max-width: 900px) {
    grid-template-columns: repeat(2, 1fr);
  }
  @media (max-width: 600px) {
    grid-template-columns: repeat(1, 1fr);
  }
`;


const rotate = keyframes`
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
`;

const LoadingSpinner = styled.div`
  border: 5px solid #f3f3f3;
  border-top: 5px solid ${theme.colors.primary2};
  border-radius: 50%;
  width: 50px;
  height: 50px;
  animation: ${rotate} 2s linear infinite;
  margin: 20px auto;
`;

로드되고 있음을 보여주기위해 카드가 로딩될때 스피너를 넣었다.

주요기능 설명

상태 정의: cards, page, hasMore 상태가 정의되어 있다. cards는 현재 화면에 표시되는 카드 목록을 저장하고, page는 현재 로드된 페이지 번호를 나타내며, hasMore는 더 로드할 카드가 있는지 여부를 나타낸다.

useInView 훅 사용: useInView 훅은 화면에 특정 요소(이 경우 LoadingSpinner)가 보이는지 감지합니다. threshold: 1은 LoadingSpinner가 완전히 보여야 inView가 true로 설정되는 것을 의미한다.
=> threshold는 useInView 또는 Intersection Observer API에서 사용되는 옵션 중 하나로, 타겟 요소가 얼마나 뷰포트(화면에 보이는 영역)에 보여야 inView 상태가 true로 변경될지를 결정하는 값이다. 이 값은 0과 1 사이의 숫자로 설정되며, 타겟 요소의 가시성 비율을 나타낸다.
=> threshold: 0 (또는 0에 가까운 값)은 타겟 요소의 일부분이라도 화면에 보이기 시작하면 inView를 true로 설정한다.
=> threshold: 1은 타겟 요소가 완전히 화면에 보여야 inView가 true로 설정된다.
예를 들어, threshold: 0.5로 설정하면 타겟 요소의 50%가 화면에 보일 때 inView 상태가 true가 된다.

카드 정렬 함수 (sortCards): sortCards 함수는 선택된 탭에 따라 카드를 정렬한다. 예를 들어, '좋아요' 수나 날짜에 따라 정렬할 수 있다. 이 함수는 useEffect 내부에서 초기 카드 로드와 페이지 변경 시 호출된다.

초기 카드 로딩 (useEffect): 컴포넌트가 마운트되거나 selectedTab이 변경될 때, sortCards 함수를 호출하여 카드를 정렬하고, 첫 번째 페이지의 카드를 로드한다.

더 많은 카드 로드 함수 (loadMoreCards): 사용자가 스크롤을 내려 더 많은 카드를 로드해야 할 때 호출된다. 이 함수는 현재 페이지 다음의 카드를 불러와 기존 카드 목록에 추가한다. 모든 카드를 로드한 경우 hasMore를 false로 설정하여 추가 로딩을 중단한다.

무한 스크롤링 구현 (useEffect): useInView에서 inView가 true로 설정되고 hasMore가 true일 때, loadMoreCards 함수를 호출하여 다음 페이지의 카드를 로드한다.

렌더링: CardGrid 컴포넌트 안에서, 현재 cards 배열의 각 카드에 대해 CardTemplate 컴포넌트를 렌더링한다. hasMore가 true이면 LoadingSpinner도 렌더링되어 추가 카드 로드가 필요할 때 useInView 훅이 활성화된다.

여기서 만약 useCallback 함수에 대해 빠르게 알고 넘어가고 싶다면? => https://velog.io/@live_in_truth/useCallback-쓱-알고가기

결과물 하나씩 가져오면서 로드될때 스피너가 돈다.

profile
참 되게 살자

0개의 댓글