# [React] 무한 스크롤 구현

React로 개발하는 웹 페이지에서 매 렌더링마다 데이터를 가져오게 된다면 불필요하게 많은 데이터 읽기가 발생할 수 있다. 이를 방지하기 위해 Pinterest 무한스크롤처럼 스크롤이 하단에 닿을 때마다 일정 개수만큼 데이터를 가져오도록 구현한 과정이다.

### Scroll Sensor 구현

```xml
<div className={styles.pageEnd} ref={pageEnd}/>
```

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699008192955/ba07f455-6c83-4cc9-8942-b0bc19325ba0.png align="center")

보이기 쉽게 빨간색으로 표현한 부분이 sensor div이다.

화면에서 sensor div가 보이는지는 IntersectionObserver api을 사용하여 관찰한다.

### IntersectionObserver 객체와 콜백함수 생성

```javascript
const pageEnd = useRef(null);
const observer = new IntersectionObserver(onIntersect, { threshold: 0 });

useEffect(() => {
    if (pageEnd.current) observer.observe(pageEnd.current);
  }, []);
```

페이지가 완전히 렌더링된 후 useEffect 안에서 observer 객체가 pageEnd.current를 대상으로 관찰할 수 있도록 렌더링한다. threshold는 대상 div의 어느 정도만큼 보일 때 함수를 호출할지를 결정하는 인자이다. (0.5면 절반이 보일 때 함수 호출)

```javascript
const onIntersect = async ([entry], observer) => {
    if (entry.isIntersecting) {
      observer.unobserve(entry.target);
      await getMorePhotos();
      setTimeout(() => {
        observer.observe(entry.target);
      }, 800);
    }
  };
```

observer 객체에게 전달하는 콜백함수는 화면과 sensor div가 intersect할 때 호출되는 함수이다.

우리는 다음 데이터를 더 로드하도록 하는 getMorePhotos()를 호출한다.

이 때, 순간적으로 콜백함수가 여러 번 호출되는 것을 막기 위해 onIntersect 안에서 잠시 observe를 해지하고, 일정시간 setTimeout 후 다시 observe를 걸어주었다.

### getMorePhotos 구현

```javascript
let timeStamp = useRef(null); 

const getMorePhotos = async () => {
    console.log("photo request");

    let queryTemp;
    if (!timeStamp) {
      //first query
      queryTemp = query(
        collection(db, "Photos"),
        orderBy("timestamp", "desc"),
        limit(10)
      );
    } else {
      queryTemp = query(
        collection(db, "Photos"),
        orderBy("timestamp", "desc"),
        startAfter(timeStamp),
        limit(10)
      );
    }
    setIsLoading(true);

    let dataSnapShot;
    try {
      dataSnapShot = await getDocs(queryTemp);
    } catch (error) {
      console.log(error);
    }

    const dataList = dataSnapShot.docs.map((doc) => doc.data());

    const length = dataList.length;
    if (length) {
      timeStamp = dataSnapShot.docs[length - 1];
      setPhotos((prev) => [...prev, ...dataList]);
    } else {
      setEndOfData(true);
    }
    setIsLoading(false);
  };
```

가져오는 데이터에는 timestamp field가 존재한다. 데이터를 시간순으로 display하기 위해 timestamp를 기준으로 쿼리를 작성한다.

```javascript
if (!timeStamp) {
      //first query
      queryTemp = query(
        collection(db, "Photos"),
        orderBy("timestamp", "desc"),
        limit(10)
      );
```

가장 처음 데이터를 읽을 때는 시간순으로 정렬된 데이터를 처음부터 (최대)10개를 읽어온다.

timestamp는 useState로 상태관리를 하려했지만, <mark>미상의 이유로 잘 작동하지 않아 useRef로 저장하며 사용하였다.</mark>

```javascript
else {
      queryTemp = query(
        collection(db, "Photos"),
        orderBy("timestamp", "desc"),
        startAfter(timeStamp), //timestamp 뒤에 있는 데이터들
        limit(10)
      );
    }
```

이후 데이터를 읽을 때는 timeStamp를 기준으로 (최대)10개의 데이터를 읽어온다.

```javascript
const length = dataList.length;
    if (length) {
      timeStamp = dataSnapShot.docs[length - 1];
      setPhotos((prev) => [...prev, ...dataList]);
    else {
      setEndOfData(true);
    }
```

그 이후 가져온 데이터의 길이를 받아 timeStamp를 업데이트한다.

만약 받아온 데이터가 10개보다 적다면 endOfData flag를 업데이트한다.

### 모든 데이터 로드가 끝났을 때

```javascript
{!endOfData && <div className={styles.pageEnd} ref={pageEnd} />}
{endOfData && (<div
          className={styles.footer}>
```

모든 데이터가 로드됐을 때는 sensor div의 관찰이 중지되어야 한다.

useEffect를 사용하여 unobserve 처리를 해주어도 좋고, 나는 단순히 요소를 제거해버렸다.

그리고 최종적으로 스크롤이 하단에 닿았으므로 그 자리에 footer를 생성한다.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1699010096331/a11e6caf-c506-4adf-8407-2c852c420e4f.png align="center")

edited by 정정환
