[리액트] 무한 스크롤 - throttle, debounce

Jang Seok Woo·2022년 2월 27일
1

리액트

목록 보기
37/58

무한스크롤을 구현하는 방법은 크게 두가지.

하나, 스크롤의 위치를 파악하여, 스크롤이 화면 하단에 도착하면 다음 데이터를 추가로 불러오는방식

둘, react-intersection-observer 사용하여 구현하는 방법

(InterSectionObserver)

  1. 스크롤 위치에 따라 구현하는 방법

스크롤 위치 + throttle(or debounce) 사용

먼저 스크롤의 위치를 감지해보자

 const {innerHeight} = window;
 const {scrollHeight} = document.body;

 const scrollTop = (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop;
        
 //전체의 높이(스크롤 전체) - (스크롤 위치 높이 + 화면 높이)
 if(scrollHeight - innerHeight - scrollTop < 200) {
     const lastId=post_list[post_list.length-1].id;
     console.log("before CallNext : ", lastId);
     callNext(lastId);
 }

화면 전체의 높이 - (스크롤의 위치 높이 + 위에서부터 현재 화면의 높이)
= 아래에 남은 높이

아래에 남은 높이가 200px이하가 되는 순간 if문을 통과하여 들어오게 된다.

들어와서 현재 로드되어있는 데이터들 중 마지막 데이터의 id값을 추출하여 서버에 다음 데이터를 요청하면 된다.

그렇다면 의문이 하나 들어야하는데

200이하가 되었을 때 if문은 계속해서 통과될 것이고 지속해서 서버에 요청을 보낼 것인가?
스크롤 하단에 남은 높이가 199에서도 보내고..
198에서도 보내고...
197에서도 보내고...

이렇게 끝없이 보내게 된다면 무분별한 요청으로 인해 비효율적이게 된다.

그럼 우리는 2가지 방법을 생각해볼 수 있는데,

  1. 스크롤이 멈춘 순간에만 데이터를 요청한다.
  2. 스크롤 하단부가 200px이하가 되었을 때 일정 시간단위로 데이터를 보낸다. (ex 0.3초마다 보내기)

그렇게 하면 스크롤이 하단이 200px이하에 위치할 때 무분별하게 계속해서 데이터 요청을 하지 않게 될 것이다.

1번의 멈춘 순간에만 데이터를 요청하는 방법은 debounce를 사용하면 된다.

debounce는 구글링을 이용하여 공부하면 된다.

2번의 스크롤 하단이 200px이하에 위치한다는 조건이 만족할 시,
일정 시간마다 요청을 보내는 방법은 throttle을 이용하면 된다.

lodash 라이브러리를 사용할 것이며 구현 코드는 다음과 같다.

InfinityScroll component

import React from "react";
import _ from "lodash";
import {Spinner} from "../elements";


const InfinityScroll = (props) => {

    const {children, callNext, is_next, loading, post_list} = props;

    
    const _handleScroll = _.throttle(() => {

        if(loading){
            return;
        }

        const {innerHeight} = window;
        const {scrollHeight} = document.body;

        const scrollTop = (document.documentElement && document.documentElement.scrollTop) || document.body.scrollTop;
        
        //전체의 높이(스크롤 전체) - (스크롤 위치 높이 + 화면 높이)
        if(scrollHeight - innerHeight - scrollTop < 200) {
            const lastId=post_list[post_list.length-1].id;
            console.log("before CallNext : ", lastId);
            callNext(lastId);
        }
    }, 300);

    const handleScroll = React.useCallback(_handleScroll, [loading]);

    React.useEffect(() => {
        
        // if(loading){
        //     return;
        // }

        if(!is_next){
            window.addEventListener("scroll", handleScroll);
        }else{
            window.removeEventListener("scroll", handleScroll);
        }
        

        return () => window.removeEventListener("scroll", handleScroll);
    }, [is_next, loading]);

    return (
        <React.Fragment>
            {props.children}
            {!is_next && (<Spinner/>)}
        </React.Fragment>
    )
}

InfinityScroll.defaultProps = {
    children: null,
    callNext: () => {},
    is_next: false,
    loading: false,
}

export default InfinityScroll;

InfinityScroll component 호출하는 xxx.js

<InfinityScroll
        
          callNext={(lastId) => {
            
            setIsLoading(true);
            apis
              .post(lastId, number)
              .then((result) => {
                
                setStateList([...stateList, ...result.data.posts]);
                setIsLast(result.data.isLast);
                
                setIsLoading(false);
              })
              .catch((error) => {
                var errorCode = error.code;
                var errorMessage = error.message;

                console.log("error catch : ", errorCode, errorMessage);
              });
          }}
          is_next={is_last}
          loading={is_loading}
          post_list = {stateList}

        >
          {stateList.map((p) => {
            if (p.nickname === user_info?.uid) {
              let like_status = false;
              if (p.like_check) {
                like_status = true;
              }

              return (
                <Grid bg="#ffffff" key={p.id}>
                  <Post is_list {...p} like_status={like_status} />
                </Grid>
              );
            } else {
              return (
                <Grid key={p.id} bg="#ffffff">
                  <Post is_list {...p} />
                </Grid>
              );
            }
          })}
        </InfinityScroll>

필요한 변수는

is_last : 데이터의 마지막인지 알려주는 변수
is_loading : 서버에 데이터를 보내 받아오는 중임을 구분하는 변수

is_last의 경우 필요한 데이터보다 1개를 더 받아오도록 구현 후, 다음에 데이터가 있다/없다를 구분하고, 마지막은 데이터 객체의 개수가 모자라면 데이터 요청을 멈추는 방식을 사용해도 된다. 위 코드는 이렇게 한 방식이 아니다.

is_last는 InfinityScroll에 is_next로 들어가 false일 경우 마지막이 아니니 계속 요청을 불러오도록 해주고, true일 경우 마지막이므로 더이상요청을 보내지 않는다.

    React.useEffect(() => {
        
        if(loading){
            return;
        }

        if(!is_next){
            window.addEventListener("scroll", handleScroll);
        }else{
            window.removeEventListener("scroll", handleScroll);
        }
        

        return () => window.removeEventListener("scroll", handleScroll);
    }, [is_next, loading]);

위와 같이, is_next 변수가 false에서 true로 변할 경우 useEffect 함수가 발동되면서 처음에 addEventListener를 달아두었던 것을 removeEventListener하게 된다.

"scroll"에 이벤트를 달아둘 것이고, 이벤트 발생시 handleScroll이라는 함수를 발동시킨다는 말인데

handleScroll 함수 구현부를 보면 useCallback을 사용하였다.

    const handleScroll = React.useCallback(_handleScroll, [loading]);

useCallback을 사용하는 이유는 해당 스크롤 이벤트 자체가 상위 컴포넌트의 '값 변동(스크롤을 내림으로 값이 추가됨)'으로 인해 여러번 불러와지게 되는데 그 것을 최초1번만 불러와서 변동사항 없으면 계속 쓰기 위함이다.
(memoization)

여기선 loading 변수가 변하면 새로 불러온다.

        <React.Fragment>
            {props.children}
            {!is_next && (<Spinner/>)}
        </React.Fragment>

InfinityScroll의 jsx 구현부를 보면 위와같이 하단에 Spinner를 달아주는 경우는 마지막이 아닐 경우. 마지막일 경우는 Spinner를 달아주지 않도록 하여 '로딩'을 표현해준다.

profile
https://github.com/jsw4215

0개의 댓글