파피루스

인피니티 스크롤(무한 스크롤) 구현 : document scroll 본문

Today I Learned

인피니티 스크롤(무한 스크롤) 구현 : document scroll

떼굴펜 2024. 6. 5. 12:22

무한 스크롤 구현하려고 하니 너무 막막해서 하나씩 적어보려고 한다.

 

0. 무한 스크롤은 어떤 순서로 동작할까?

일단 무한 스크롤의 동작을 상상해보았다. 아래처럼 움직이면 되지않을까?

  // 1. 최초 list 로딩
  // 2. scroll 최하단 감지
  // 3. hasNextPage 여부 체크 -> false일 때 fetch 요청하지 않도록
  // 4. fetch해서 리스트 불러오기
  // 5. 화면에 뿌리기 ==> react는 fetch한 데이터 set하면 된다. 

 

1. 최초 list 로딩

  const [posts, setPosts] = useState([]);
 
  useEffect(() => {
    (async () => {
      api.getRecentPosts().then((list) => {
        setPosts((posts) => [...posts, ...list]);
      });
    })();
  }, []);

 

 

2. scroll 최하단 알아내기

최하단에서 5px 이하로 남아있으면 데이터를 추가로 가져와서 뿌려줬으면 좋겠다.
남은 스크롤 길이는 어떻게 알아내지?

 

react 무한 스크롤 검색하면 코드를 쭉 볼 수 있겠지만, 난 내가 직접 짜보고 더 좋은 답을 보고 싶었다.

그래서 scrollable height 를 구글링했고 몇가지 keyword를 찾아냈다. 

 

- window.scrollY : 현재 스크롤 y축 위치

- document.documentElement.scrollHeight - document.documentElement.clientHeight
: 찾았다! 남은 스크롤 가능한 길이 

 

 

scroll event를 우선 걸고, 최하단 길이가 5이하일 때만 동작할 수 있도록 분기처리를 해주었다.

 
  useEffect(() => {
    window.addEventListener('scroll', handleScrollEvent);
  return () => {
      window.removeEventListener('scroll', handleScrollEvent);
    };
  }, []);

  const handleScrollEvent = useCallback(async () => {
    const element = document.documentElement;
    const scorllableHeight = element.scrollHeight - element.clientHeight;
    const restScrollHeight = scorllableHeight - scrollY;
    if (restScrollHeight > 5) {
      return;
    }
    // 데이터 받아와서 뿌려주자!!!
  }, []);
 

 

 

참고) 직접 스크롤해보면서 안에 무엇이 들어있는지 메모해보았다.

   const element = document.documentElement;
    // body height = element.scrollheight = clientHeight + scrollTop
    // window.scrollY = element.scrollTop : 실제 유저의 스크롤 높이
    // element.scrollHeight - element.clientHeight = 스크롤할 수 있는 최대 height

    window.addEventListener('scroll', () => {
      console.log(
        '==> ',
        element.clientHeight,
        element.scrollTop,
        element.scrollHeight,
        element.clientHeight + element.scrollTop,
        element.scrollHeight - element.clientHeight
      );

      console.log(scrollY);
    });
  }, []);

 

 

3. hasNextPage 여부 체크 -> false일 때 fetch 요청하지 않도록 

a. hasNextPage 가 true면 scroll event 중단 (데이터 불러오지 말것)
b. 새로 불러온 list.length ===0 이면 hasPage = false 로 변경
  const [pageNo, setPageNo] = useState(0);
  const [posts, setPosts] = useState([]);
  const [hasNextPage, setHasNextPage] = useState(true);

  useEffect(() => {
    (async () => {
      api.getRecentPosts({ startNo: pageNo, rownum: POSTS_ROWNUM }).then((list) => {
        if (list.length === 0) {
          setHasNextPage(false);
          return;
        }
        setPosts((posts) => [...posts, ...list]);
      });
    })();
  }, []);
 

  const handleScrollEvent = useCallback(async () => {
    const element = document.documentElement;
    const scorllableHeight = element.scrollHeight - element.clientHeight;
    const restScrollHeight = scorllableHeight - scrollY;
    if (!hasNextPage || restScrollHeight > 5) {
      return;
    }
    
// 데이터 받아와서 뿌려주자!!!
  }, []);

 

 

4. fetch해서 데이터 갖고오기

1. API를 통해서 posts를 갖고오는데, paging처리를 할 수 있도록 변경했다.

2. 몇번째 줄(startNo)를 넘기게 되면서, fetch하는 부분이 pageNo가 바뀔때마다 실행되도록 dependency를 넣어주었다.

3. scroll event 시 데이터 받아올 때 pageNo를 바꿔주면 fetch가 트리거된다 ?! -> 바로 setPageNo 때려박기!

  const [pageNo, setPageNo] = useState(0);
  const [posts, setPosts] = useState([]);
  const [hasNextPage, setHasNextPage] = useState(true);
  const
 ROWNUM = 13;

  useEffect(() => {
    (async () => {
      api.getRecentPosts({ startNo: pageNo, rownum: ROWNUM  }).then((list) => {
        if (list.length === 0) {
          setHasNextPage(false);
          return;
        }
        setPosts((posts) => [...posts, ...list]);
      });
    })();
  }, [ pageNo ]);

  const handleScrollEvent = useCallback(async () => {
    const element = document.documentElement;
    const scorllableHeight = element.scrollHeight - element.clientHeight;
    const restScrollHeight = scorllableHeight - scrollY;
    if (!hasNextPage || restScrollHeight > 5) {
      return;
    }
    setPageNo((pageNo) => pageNo + ROWNUM   + 1);
  }, []);

  useEffect(() => {
    window.addEventListener('scroll', handleScrollEvent);
    return () => {
      window.removeEventListener('scroll', handleScrollEvent);
    };
  }, []);

 

 


5. list 뿌려주기! 

이미 fetch할 때 list 값이 있으면 setPosts하도록 만들어져 있어 수정할 것이 없다! 완성!

  const [pageNo, setPageNo] = useState(0);
  const [posts, setPosts] = useState([]);
  const [hasNextPage, setHasNextPage] = useState(true);
  const
 ROWNUM = 13;

  useEffect(() => {
    (async () => {
      api.getRecentPosts({ startNo: pageNo, rownum: ROWNUM  }).then((list) => {
        if (list.length === 0) {
          setHasNextPage(false);
          return;
        }
        setPosts((posts) => [...posts, ...list]);
      });
    })();
  }, [ pageNo ]);

  const handleScrollEvent = useCallback(async () => {
    const element = document.documentElement;
    const scorllableHeight = element.scrollHeight - element.clientHeight;
    const restScrollHeight = scorllableHeight - scrollY;
    if (!hasNextPage || restScrollHeight > 5) {
      return;
    }
    setPageNo((pageNo) => pageNo + ROWNUM   + 1);
  }, []);

  useEffect(() => {
    window.addEventListener('scroll', handleScrollEvent);
    return () => {
      window.removeEventListener('scroll', handleScrollEvent);
    };
  }, []);

 

 

 

 

 

 

참고한 글) 

- https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight

 

Element: scrollHeight property - Web APIs | MDN

The Element.scrollHeight read-only property is a measurement of the height of an element's content, including content not visible on the screen due to overflow.

developer.mozilla.org

- https://velog.io/@sanghyeon/React%EC%97%90%EC%84%9C-Scroll-Event-%EA%B0%90%EC%A7%80%ED%95%98%EA%B8%B0

'Today I Learned' 카테고리의 다른 글

supabase, 로그인 시 localhost로 가버린다면?  (0) 2024.06.06
vercel, 도메인 연결  (0) 2024.06.06
commit convention  (0) 2024.06.04
React hook (보강 메모)  (0) 2024.05.31
[React] 전역변수 관리 (props drilling)  (0) 2024.05.24