무한스크롤 구현

차차·2023년 1월 31일
0
post-thumbnail

이번 프로젝트에서 스터디 일지부분을 무한스크롤을 통해 원하는 게시글 수만큼을 추가로 불러오고 싶다.

이전에 사용했던 방법은 react-custom-scrollbars-2에서 제공하는 onScrollFrame이라는 속성을 사용해 스크롤 위치가 top인지를 확인하는 방법으로 데이터를 불러왔었다. ( throttle 조차 사용하지 않음)

하지만 이러한 방법은 브라우저 성능을 저하시킬 수 있다는 얘기가 있어 Intersection Observer API를 사용해 무한스크롤을 구현해보고자 한다.


👇 아래는 Intersection Observer API 사용방법이다.
https://velog.io/@chacha_fe/Intersection-Observer-API


useObserver 커스텀훅 구현

import { useEffect, useRef, useState } from "react";

type Callback = () => void;

const useObserver = (callback: Callback) => {
  const targetRef = useRef<HTMLDivElement>(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    if (!targetRef.current) return;

    const io = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (
            entry.isIntersecting &&
            entry.intersectionRatio === 1 &&
            !loading
          ) {
            setLoading(true);
            callback();
            setLoading(false);
          }
        });
      },
      {
        root: targetRef.current?.parentElement,
        threshold: [1],
      }
    );

    io.observe(targetRef.current);

    return () => {
      io.disconnect();
    };
  }, [callback, loading]);
  return [targetRef];
};

export default useObserver;
  1. 대상 요소가 root 요소와 전부 겹치게 되었을 때 실행할 callback 함수를 인자로 받는 useObserver.ts를 생성
  2. 대상 요소를 ref할 targetRef를 생성
  3. useObserver가 실행중인지 아닌지를 확인할 boolean 값 loading state를 생성
  4. 대상이 isIntersecting = true (교차 상태)이며 intersectionRation = 1 (100% 가시되고 있으며) loading = false (useObserver가 실행되지 않고 있다면) callback 을 실행한다.
  5. root 대상 요소의 부모 요소이다.
  6. io.observe(targetRef.current) 를 통해 대상요소를 관측한다.
  7. 언마운트 될 때 모든 관측을 중지한다.
  8. targetRef를 리턴한다.



useObserver.ts 사용

const scrollRef = useRef<Scrollbars>(null);
  const [page, setPage] = useState(0);
  const [diarys, setDiarys] = useState<Diarys>([]);
  const [scrollHeight, setScrollHeight] = useState<number>(0);

  const getPage = useCallback(() => {
    if (!scrollRef.current) return;
    setPage((prev) => prev + 1);
  }, []);

	const [targetRef] = useObserver(getPage);

	...

	return (
	****<Scrollbars autoHide ref={scrollRef}>
	  <div ref={targetRef} />
	  <div className="flex flex-col px-[3rem] ">
	    {Object.entries(DiarySections || {})?.map(([day, diarys]) => (
	      <div
	        key={day}
	        className="flex flex-col space-y-[2.8rem] border-b py-[2.8rem]  last:border-none"
	      >
	        <span className="flex-center Sub2 w-[11.6rem] rounded-full bg-primary-500 py-[0.6rem] text-primary-100">
	          {day}
	        </span>
	        <ul className="flex flex-col space-y-[1.4rem]">
	          {diarys?.map((diary) => (
	            <DiaryItem key={diary.userId} diary={diary} />
	          ))}
	        </ul>
	      </div>
	    ))}
	  </div>
</Scrollbars>
	)

const [targetRef] = useObserver(getPage);

스크롤을 가장 위로 올렸을 때 page를 1씩 더해주는 함수 getPage 를 인자로 전달한다.


<Scrollbars autoHide ref={scrollRef}> 
	<div ref={targetRef} />
	...
</Scrollbars>

감시할 대상에게 targetRef를 적용시킨다.

감시할 대상의 부모 요소인 Scrollbars 가 root 요소가 된다. 스크롤을 올려 감시 요소가 전부 보여졌을 때

useObserver 에 전달한 getPage 가 실행된다.




❓ 

한 가지 문제점이 있다. 스크롤의 가장 위로 올라갔을 때 데이터가 추가된다면 스크롤의 위치가 들어온 데이터만큼 내려와야 하는데 최상단에 가만히 멈춰있다. 이를 해결하기 위해서는 추가된 데이터들의 높이만큼 스크롤 계산을 해주어야 한다.


스크롤 계산하기

  1. 스크롤이 root 요소의 가장 최상단에 닿았을 때 즉, 감시 요소가 전부 보여졌을 때 page 를 1씩 증가시키는 함수를 callback으로 전달하였다. 이때 현재 root의 높이를 저장한다.

    const getPage = useCallback(() => {
        if (!scrollRef.current) return;
        setPage((prev) => prev + 1);
    		// 현재 root의 높이
        setScrollHeight(scrollRef.current.getScrollHeight());
      }, []);

  1. 데이터가 추가된 후에 root의 높이 - 저장한 root의 높이만큼 스크롤 최상단에서 내려오게한다.

    useEffect(() => {
    	// 첫 마운트 되었을 때 실행시키지 않게 하기 위해서 처리
      if (DiarySections.length <= 10 || !scrollRef.current) return;
    	// 데이터가 들어온 후에 root 높이 
      let currentHeight = scrollRef.current.getScrollHeight();
    	// 이전과 현재의 root 높이가 같다면 실행 x (굳이 필요할까 싶기도하다.)
      if (scrollHeight === currentHeight) return;
    	//scroll 위치를 root 최상단에서 현재 - 이전 높이만큼 멀어지게한다.
      scrollRef?.current.scrollTop(currentHeight - scrollHeight);
    	// 데이터가 변경될 때마다 실행 
    }, [DiarySections]);



아직 서버와 맞춰보지 않아서 확실하진 않지만 한번 수정을 더 해야겠다.. !
profile
나는야 프린이

0개의 댓글