TIL #1 - 빠르고 간편하게 React 로 InfiniteScroll (무한 스크롤) 구현하기

Tami·2021년 7월 27일
15

TIL

목록 보기
2/9
post-thumbnail

React로 InfiniteScroll을 하는 방법 2가지

  1. onScroll 이벤트
  2. Intersection Observer API

onScroll과 그 한계


사용자의 scroll event를 계속 보며 페이지가 끝에 오는지를 판단하는 방법을 구현이다. 하지만
드르륵 스크롤 할. 때마다 이벤트가 발생하는 것이다.

크롬에다 이것만 쳐봐도 알 수 있다.

window.addEventListener('scroll',()=>console.log('🌀'))

그렇다면 남은 것은

🥳 Intersection Observer API

타겟과 viewPort 사이의 intersection 변화를 비동기적으로 관찰하는 방법

  • LazyLoading
  • Infinite-scroll
  • 사용자 결과 여부에 따른 애니메이션

등에 사용된다.

여기서 나는 Infinite-scroll을 만들며 API를 겪어보았다.

Infinite-Scroll 구현 순서

  1. 스크롤할 Component 구성
  2. Intersection observer 생성
  3. 생성한 Intersection observer 붙여주기
  4. useFetch 생성 (data가 API요청일 때)

스크롤할 대상을 만든 후
Intersection observer를 만들어서 ref로 끝나는 지점에 붙여줄 것이다!
현재 data는 10개씩 API query 요청하여 불러오고 있다.

파일은 크게 두가지
- useFetch(커스텀 fetch Hook)
- CardList(스크롤할 component)



1) 스크롤할 Component 구성

CardContainer 가장 아래에 빈 div를 생성해 ref를 달아준다.
이곳에서 교차시점을 알아볼 것이기 때문

<CardListContainer>
      {list?.map((card) => (
        <Cardd>{card.id}</Cardd>
      ))}
      <div ref={observer} />
      <>{isLoading && <Loading />}</>
</CardListContainer>

2) Intersection observer 생성 (예시)

참고) options는 옵션사항이라 예시코드에서는 따로 넣지 않았다.

let options = {
  root: document.querySelector('#scrollArea'),
  rootMargin: '0px',
  threshold: 1.0
}

let observer = new IntersectionObserver(callback, options);

root : 스크롤 할 대상을 감싼 요소 , 스크롤 할 곳
null 혹은 default = viewPort

rootMargin : root 가 가진 여백
문자열로 넣기

threshold : target이 root와 몇% 교차했을 때 callback 을 실행할 지 결정하는 값
0.0~1.0

3) 생성한 Intersection observer 붙여주기

pageNum:페이지번호를 query 로 API 요청해 data를 받아오기 때문에 pageNum을 useFetch에 넘겨줌
list : 뿌려줄 data
isLoading: 로딩중인지 boolean
hasMore : data 더 가져올 게 남았는지

< CardList.jsx >

const CardList = () => {
  const [pageNum, setPageNum] = useState(1);
  const { list, hasMore, isLoading } = useFetch(pageNum);
  const observerRef = useRef();

  const observer = (node) => {
    if (isLoading) return;
    if (observerRef.current) observerRef.current.disconnect();

    observerRef.current = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting && hasMore) {
        setPageNum((page) => page + 1);
      }
    });

    node && observerRef.current.observe(node);
  };

  return (
    <CardListContainer>
      {list?.map((card) => (
        <Cardd>{card.id}</Cardd>
      ))}
      <div ref={observer}/>
      <>{isLoading && <Loading />}</>
    </CardListContainer>
  );
};
export default CardList;

메인이 되는 부분만 주석을 달아 설명하자면

const observerRef = useRef();

  const observer = (node) => {
    // 1.로딩중이면 return
    if (isLoading) return;
    //onserverRef.current가 있으면 끊기
    if (observerRef.current) observerRef.current.disconnect();
    //observerRef.current 생성
    observerRef.current = new IntersectionObserver(([entry]) => {
      // entry 가 교차되었으며, 데이터가 더 있으면 페이지 1증가
      if (entry.isIntersecting && hasMore) {
        setPageNum((page) => page + 1);
      }
    });
    
    //
    node && observerRef.current.observe(node);
  };

4) useFetch 생성

< useFetch.jsx>

import React, { useState, useEffect, useCallback } from 'react';

const END_POINT = 'https://';
const useFetch = (page) => {
  const [list, setList] = useState([]);
  const [hasMore, setHasMore] = useState(false);
  const [isLoading, setIsLoading] = useState(false); //로딩 구현 시에만 필요

  //query API 요청 보내기
  const sendQuery = useCallback(async () => {
    const URL = `${END_POINT}?${page}~~~`;
    
    try {
      setIsLoading(true);
      const response = await (await fetch(URL)).json();
      if (!response) {
        throw new Error(`서버에 오류가 있습니다.`);
      }
      setList((prev) => [...new Set([...prev, ...response])]);
      setHasMore(response.length > 0);
      setIsLoading(false);
    } catch (e) {
      throw new Error(`오류입니다. ${e.message}`);
    }
  }, [page]);

  useEffect(() => {
    sendQuery();
  }, [sendQuery, page]);

  return { hasMore, list, isLoading };
};
export default useFetch;
// list. data 전에 있던 값 + 새로 가져온 query API 응답값을 더해 새로운 배열을 만들어 set
      setList((prev) => [...new Set([...prev, ...response])]);
//응답값이 있으면 set
      setHasMore(response.length > 0);
// data 불러오기 전 잠시 로딩중
    setIsLoading(true);
// 중략
    setIsLoading(false);

구현 결과

무한 스크롤

로딩중


✨ 여기서 보너스

useCallback

React 에서 component가 렌더링 될 때마다 내부에 선언된 표현식도 다시 선언되는데, 이것은 매우 낭비
이것을 방지하기 위해 useCallback을 사용할수 잇는데
1째 마운트 될 때 한번만 선언하고 그 이후로 재사용하자!

  • 이벤트 핸들러 함수

  • api를 요청하는 함수에 주로 사용한다.


    명심하자, 그래 메모리가 다돈이다~


참고 블로그1
참고 블로그2
참고 블로그3
좋은 블로그
🔗 Tami 다른 블로그
https://rrecoder.tistory.com/

profile
자스베이더 Tami의 TILAND에 오신걸 환영합니다🗡

2개의 댓글

comment-user-thumbnail
2021년 7월 28일

오~ 스크롤을 내리다가 추가할 시점이 되면 페이지를 하나 추가해 주는 거군요 !

답글 달기
comment-user-thumbnail
2021년 7월 28일

intersection observer라는게 있었네요! it works for me 😄

답글 달기