이미 봐버린 이상 해야한다...
벨로그 홈페이지를 보면 데이터를 한번에 가져오는 것이 아닌 화면 스크롤 시 실시간으로 가져옴
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-쓱-알고가기