072DATA

`React` 무한 스크롤 만들기 (React ,Supabase, intersectionObserver) 본문

FrontEnd/React

`React` 무한 스크롤 만들기 (React ,Supabase, intersectionObserver)

0720 2024. 9. 4. 23:58

안녕하세요

 

오늘은 팀 프로젝트를 진행하면서 구현했던 무한스크롤 방식을 TIL로 작성해보려 합니다

무한 스크롤은 꼭 한 번 구현하고 싶었던 기능이었기 때문에 설레는 마음으로 기능을 구현하였습니다.

intersectionObserver을 React에서 사용하고 Supabase로 데이터를 받아왔습니다!

1. 상태 변수 설정

 

  const [postLoadingMore, setPostLoadingMore] = useState(true);
  const [displayedPosts, setDisplayedPosts] = useState([]);
  const [offset, setOffset] = useState(0);
  const limitLength = 6;

 

  • postLoadingMore: 추가 데이터가 있는지에 대한 상태를 관리
  • displayedPosts: 현재 화면에 표시된 게시물
  • offset: 데이터베이스에서 불러올 게시물의 시작 인덱스
  • limitLength: 한 번에 가져올 게시물의 수 ( 6개씩)

 

2.옵저버 설정

  const handleObserver = (entries) => {
    if (entries[0].isIntersecting && !postLoadingMore) {
      setPostLoadingMore(true);
    }
  };

 

 

IntersectionObserver의 콜백함수가 될 handleObserver는

사용자가 페이지 하단에 도달했을 때, postLoadingMore

상태를 true로 설정하여 추가 게시물 로딩을 실행

 

  const observerRef = useRef(null);

  const observer = new IntersectionObserver(handleObserver, {
    threshold: 0.7
  });
  
  
  // 감지하는 요소의 ref에 observerRef 추가
  
<ObserverArea ref={observerRef}>
    {displayedPosts.length === posts.length ? 
    // 현재 렌더링된 게시물의 길이와 전체 게시물의 길이가 같으면 게시물 없음 표시
    (
      <div className="emptyArea">게시물이 없습니다.</div>
    ) : (
      <>
        <Spinner />
      </>
    )}
</ObserverArea>
IntersectionObserverEntry 객체 정리

    boundingClientRect: 요소의 크기와 위치를 나타내는 DOMRect 객체
    intersectionRect: 요소와 뷰포트가 교차하는 영역을 나타내는 DOMRect 객체
    intersectionRatio: 요소가 뷰포트와 교차하는 비율입니다.
    isIntersecting: 요소가 뷰포트와 교차하는지 (닿는지) 여부를 나타내는 불리언 값
    //지금은 isIntersecting 이 객체의 속성을 사용!!( 닿는지 여부만 판단하면 됨)
    rootBounds: 루트 요소의 DOMRect 객체
    target: 관찰 중인 요소를 참조하는 Element

 

 


useRefIntersectionObserver로 감지하는 요소를 지정하고 해당 요소에 옵션 값을 설정함

thresholdIntersectionObserver의 옵션의 속성 값으로 

0 기준으로 보이기만 하면 바로 감지 / 1 로 갈수록 감지 영역이 적어짐

 

3. 초기 게시물 로딩 (useEffect)

 

Supabase 데이터 요청 함수

  const getSixPosts = async (limit, offset) => {
    const { data, error } = await supabase
      .from("posts")
      .select("*")
      .order("created_at", { ascending: false })
      .range(offset, offset + limit - 1);

    if (error) {
      console.error("게시물 불러오기 에러:", error);
    }

    return { data };
  };

 

컴포넌트에서 데이터 로드

  useEffect(() => {
    const loadInitialPosts = async () => {
      getAllPosts();

      if (!displayedPosts) {
        const { data } = await getSixPosts(limitLength, offset);
        setDisplayedPosts(data);
        setOffset(offset + limitLength);
      } else if (displayedPosts && offset !== 0) {
        const { data } = await getSixPosts(limitLength, 0);
        setDisplayedPosts(data);
        setOffset(0 + limitLength);
      }
    };

    loadInitialPosts();
  }, [isView]);

 

 

displayedPosts에 값이 없으면 초기 데이터로 게시글을 6개씩 가져오는데

supabase에서 제공하는 range라는 SQL문을 사용하여

offset = 시작 인덱스, limitlLength = 가져올 게시 글의 갯수로 초기에 설정한 0과 6으로 6개를 가져옴

그래서 getSixPosts를 컴포넌트에서 실행할 때 인자로 전달하고 Offset의 상태를 변경하여

이미 가져온 데이터를 중복 방지하기 위해 가져온 만큼 길이를 늘려줌 (물론 초기데이터 로드라 6이라고 써도 됨)

 

그 다음 잘 보면 useEffect의 의존성 배열에 isView라는게 존재하는데

사실 해당 페이지는 뉴스피드 형식의 커뮤니티로 뷰 방식그리드와 1열 뷰 방식이 있기 때문에

해당 버튼이 눌렸을 때 다시 초기 데이터 값으로 6개부터 렌더링 하기 위해 else if 문으로 

else if (이미 초기 값이 들어간 상태) 에서 isView의 상태가 변경될 떄 리렌더링 되도록 로직을 추가했다.

 

그리하여 이 블로그를 요약하는 사람들은 else if문의 로직과 의존성 배열에 있는 isView를 무시하면 된다!

 

4. 추가 게시물 로딩 (useEffect)

 

  useEffect(() => {
    if (postLoadingMore) {
      const loadMorePosts = async () => {
        const { data } = await getSixPosts(limitLength, offset);
        setDisplayedPosts((prevPosts) => [...prevPosts, ...data]);
        setOffset(offset + limitLength);
        setPostLoadingMore(false);
      };

      loadMorePosts();
    }

    if (observerRef.current) {
      observer.observe(observerRef.current);
    }

    return () => {
      if (observerRef.current) {
        observer.unobserve(observerRef.current);
      }
    };
  }, [postLoadingMore]);

 

먼저 의존성 배열을 살펴보면 postLoadingMore 이라는 상태가 들어있다 

처음  상태 변수를 설정하고 IntersectionObserver의 콜백함수 안에 setpostLoadingMore를 사용하여

 

내가 감지하고자 하는 요소의 영역이 감지 되었을 때 콜백함수가 실행되어 postLoadingMore의 상태를 변경하고

해당 상태가 변경되면 의존성 배열로 참조하던 useEffect가 리렌더링되는 것임

 

로직을 보면 이미 초기 데이터가 리렌더링되어 offSet의 값이 6이 되었을 것이고

offSet 번째 인덱스 부터 다시 6개의 게시글을 getSixPosts 함수를 사용해 데이터를 받아온뒤

그 다음 로딩을 대비하여 가져온 길이만큼 offSet의 값을 설정해 주는 것이다

 

그 후 if 문으로 observer가 감지하는 영역에 요소(우리의 화면 시점이 닿는지)가 존재하는지 존재하지 않는지 

판단하여 observer를 키고 return문에 useEffect를 나오기 전 observer를 꺼준다.

 

무한 스크롤 동작 요약

 

 

  • 초기에는 페이지가 로드될 때 첫 6개의 게시물이 표시됨
  • 사용자가 페이지 하단에 도달하면 옵저버가 켜지고 추가 게시물을 비동기로 가져옴
  • 따라서 사용자 스크롤에 따라 게시물이 자동으로 로드되는 무한 스크롤이 구현됨

 

 

시연 연상

 

잘 나오쥬

 

 

마치면서

 

해당 로직으로 리팩토링 하기 전 무한 스크롤이 아닌 무한 로딩이 되는 버그가 나온다던가

글 작성시 새로운 게시글을 렌더링 하지 못하는 오류에 대한 포스팅은 내일 작성하도록 하겠습니다.

 

왜냐하면 오늘은 프로젝트 발표도 끝났고 너무 귀찮거든여 구현 방식만 작성할게요 에?

그리고 무한 스크롤을 구현하기 전에는 정말 어렵고 힘든 작업인 줄 알았는데 정말 그렇더라구요

 

가 아니라 생각보다 로직을 잘 짜서 조건부 렌더링을 잘 해 놓으면 오류 없이 기능이 잘 구현 되어서

평소에 생각하던 어려울 것 같은 기능들에 조금이나마 자신감이 생긴 것 같습니다.