React 무한 스크롤 적용 - TIL #7

날림·2022년 2월 23일
2

TIL

목록 보기
7/7
post-custom-banner

GCP 과금의 압박도 있고
부트캠프 수료 후 프로젝트 손볼 시간 여유가 없어질 듯 하여
2022년 2월 마지막 주, 열심히 마지막 기능추가를 해보고 있습니다

이번 포스팅은 챌린지 목록 페이지에 무한 스크롤을 적용해본 내용입니다.


참고한 곳들

호주 정부 api 기준
부트캠프 중간 시험 중 참고하라고 알려준 페이지

React – Infinite Scroll, 무한 스크롤 구현하기
기본 구현 방식을 여기서 참고했습니다

요소 사이즈와 스크롤
scrollTop, clientHeight, scrollHeight 등등
요소와 스크롤 관련 위치 정보를 알 수 있는 페이지입니다

[javascript] useState 설정 메소드가 즉시 변경 사항을 반영하지 않음
useState의 값을 변경하는 함수를 사용해도 state가 바로 바뀌지 않아서 해맬 때 도움이 되었습니다


1. api 수정

client/src/apis/index.js

export const requestPopularChallenges = (limit, page, query) => {
  let string = `${process.env.REACT_APP_API_URL}/challenges/popular?`;
  string += `${limit > 0 ? 'limit=' + limit + '&' : ''}`;
  string += `${page ? 'page=' + page + '&' : ''}`;
  string += `${query ? 'query=' + query : ''}`;

  return axios
    .get(
      string,
      {},
      {
        'Content-Type': 'application/json',
      }
    )
    .then((result) => result.data.data);
};

화면 바닥까지 스크롤 될 때마다
다음 페이지의 결과를 받아올 수 있도록
먼저 페이지 query를 넣어줍니다

예시는 인기순 api지만 최신순도 마찬가지로 수정


2. 챌린지 목록 페이지

client/src/pages/challenges.js
길어서 부분 부분 잘라서 설명하겠습니다

  const [sorting, setSorting] = useState('latest');
  const [challenges, setChallenges] = useState([]);
  const [page, setPage] = useState(1);
  const [query, setQuery] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [hasNoResult, setHasNoResult] = useState(false);
  const [hasNoMoreResult, setHasNoMoreResult] = useState(false);

이 페이지가 useState로 관리하는 상태입니다
현재 받아온 챌린지가 몇 페이지인지는 page로 표시하고,
더 이상 받아올 챌린지가 없을 때는
hasNoMoreResulttrue로 바꿔 저장합니다

  const fetchNextData = () => {
    if (!hasNoMoreResult) {
      setIsLoading(true);
      setPage(page => page + 1);
    }
  };

다음 페이지를 받아오도록 하는 함수입니다
하지만 이 안에 fetch하는 기능은 없습니다
setPage(page => page + 1);로 페이지만 바꿔줄 뿐인데...

  useEffect(() => {
    async function fetchNextPage () { 
      const scrollY = window.scrollY;
      if (page > 1) {
        if (sorting === 'latest') {
          const result = await requestLatestChallenges(20, page, query);
          setChallenges(challenges => [...challenges, ...result]);
          if (result.length < 20) setHasNoMoreResult(true);
        } else {
          const result = await requestPopularChallenges(20, page, query);
          setChallenges(challenges => [...challenges, ...result]);
          if (result.length < 20) setHasNoMoreResult(true);
        }
        setIsLoading(false);
        window.scrollTo(0, scrollY);
      }
    }
    fetchNextPage()
    // eslint-disable-next-line
  }, [page])

다음 페이지의 챌린지 정보를 실제로 가져오는 React Hook입니다

  • fetchNextDatapage가 바뀔 때마다 실행되어
    다음 페이지의 챌린지 정보를 가져와 합칩니다
  • const scrollY = window.scrollY;스크롤 된 높이를 저장하고,
    window.scrollTo(0, scrollY);로 저장된 높이만큼 스크롤 시켜
    움직이지 않은 것 처럼 보이게 합니다
  • 첫 페이지에서는 동작하지 않게 합니다 - 스크롤 전 & 검색 후 결과 등등
  • 가져온 결과가 20개 이하라면, 다음 페이지가 없기 때문에
    if (result.length < 20) setHasNoMoreResult(true);

        <ChallengeList>
          <InfiniteScroll
            data={challenges}
            type='challenge'
            isLoading={isLoading}
            fetchNextData={fetchNextData}
          />
        </ChallengeList>

return 안 무한 스크롤 컴포넌트입니다
props로는

  • 보여줄 data
  • datatype (여기서는 없어도 되지만 나중을 위해서)
  • isLoading으로 중복 요청 방지
  • 무한 스크롤 컴포넌트 내에서
    다음 페이지 정보를 요청하기 위한 fetchNextData

를 내려줍니다


3. 무한 스크롤 컴포넌트

client/src/components/InfiniteScroll.js

전체 코드는 이렇습니다

import React, { useEffect } from 'react';
import ChallengeCard from './ChallengeCard';

const InfiniteScroll = ({ data, type, isLoading, fetchNextData }) => {
  const handleScroll = () => {
    const scrollHeight = document.documentElement.scrollHeight;
    const scrollTop = document.documentElement.scrollTop;
    const clientHeight = document.documentElement.clientHeight;
    if (scrollTop + clientHeight >= scrollHeight && !isLoading) {
      fetchNextData();
    }
  };

  const throttle = (func, waits) => {
    let lastFunc; // timer id of last invocation
    let lastRan; // time stamp of last invocation
    return function (...args) {
      const context = this;
      if (!lastRan) {
        func.apply(context, args);
        lastRan = Date.now();
      } else {
        clearTimeout(lastFunc);
        lastFunc = setTimeout(() => {
          if (Date.now() - lastRan >= waits) {
            func.apply(context, args);
            lastRan = Date.now();
          }
        }, waits - (Date.now() - lastRan));
      }
    };
  };

  useEffect(() => {
    const clientWidth = document.documentElement.clientWidth
    const clientHeight = document.documentElement.clientHeight;
    if (clientWidth > 1024 && (60 + 16 + 40 + 70) + 954 < clientHeight) {
      fetchNextData();
    }
    window.addEventListener('scroll', throttle(handleScroll, 500))
    return () => {
      window.removeEventListener('scroll', throttle(handleScroll, 500))
    };
  // eslint-disable-next-line
  }, []);


  return (
    <div>
      {data && type === 'challenge'
        ? data.map((el, index) => (
            <ChallengeCard
              challenge={el}
              key={index}
            />
          ))
        : null}
    </div>
  );
};

export default InfiniteScroll;

여기서도 조금 잘라서 보면,
useEffect(() => {
    const clientWidth = document.documentElement.clientWidth
    const clientHeight = document.documentElement.clientHeight;
    if (clientWidth > 1024 && (60 + 16 + 40 + 70) + 954 < clientHeight) {
      fetchNextData();
    }
    window.addEventListener('scroll', throttle(handleScroll, 500))
    return () => {
      window.removeEventListener('scroll', throttle(handleScroll, 500))
    };
  // eslint-disable-next-line
  }, []);
  • 클라이언트 화면이 넓어서 챌린지가 20개 이상 들어가고도 남는 경우,
    clientWidth > 1024 && (60 + 16 + 40 + 70) + 954 < clientHeight
    정보를 한번 더 요청해 화면을 꽉 채울 수 있도록 합니다
  • window.addEventListener('scroll', throttle(handleScroll, 500))
    useEffect로 컴포넌트 마운트 시, scroll 이벤트에 handleScroll 함수를 달아줍니다
    - throttle0.5초에 한 번씩만 동작하도록 제한을 둡니다
    (scroll은 한 번에 굉장히 많이 발생하는 이벤트로
    부담이 심할 수 있습니다)
  • return () => { window.removeEventListener('scroll', throttle(handleScroll, 500)) };
    - 컴포넌트가 언마운트 될 때, scroll 이벤트에 달았던 handleScroll 함수를 제거합니다

  const handleScroll = () => {
    const scrollHeight = document.documentElement.scrollHeight;
    const scrollTop = document.documentElement.scrollTop;
    const clientHeight = document.documentElement.clientHeight;
    if (scrollTop + clientHeight >= scrollHeight && !isLoading) {
      fetchNextData();
    }
  };

요소 사이즈와 스크롤
scrollTop, clientHeight, scrollHeight 등등
요소와 스크롤 관련 위치, 크기 정보를 알 수 있는 페이지입니다

  • scrollHeightscrollTop, clientHeight를 비교하여 화면 하단까지 스크롤 되었는지 판단합니다
  • 하단까지 스크롤 되었고, !isLoading으로 요청 중이 아니라고 정해졌다면 fetchNextData()로 다음 페이지 정보를 요청합니다

  return (
    <div>
      {data && type === 'challenge'
        ? data.map((el, index) => (
            <ChallengeCard
              challenge={el}
              key={index}
            />
          ))
        : null}
    </div>
  );
};
  • props로 전달받은 data를 형식에 맞게 표현합니다

결과

1

뿌듯


앞으로 할 일

서버와 배포 상황에 맞게 수정한 후
업로드하고 잘 적용 되었는지 확인

profile
항상배우기
post-custom-banner

0개의 댓글