---
# System prepended metadata

title: 무한 스크롤
tags: [tech sharing]

---

# 무한 스크롤

###### tags: `tech sharing`

SWR을 이용해서 댓글을 무한 스크롤을 구현하려고 하는데 과정은 크게 두 가지로 나눌 수 있었다.

1. useSWRInfinite를 이용해서 페이지 단위로 데이터를 로딩하기.
2. 제일 마지막 댓글이 보여지면 다음 페이지 로딩하도록 하기.

`useSWRInfinite`의 반환값에 현재 로딩한 페이지를 나타내는 size state가 있고 이 state를 set 하는 setSize가 있었다. setSize를 이용해서 size를 변경시켜주면서 다음 페이지의 데이터를 로딩했다.

스크롤이 끝에 다다랐는지를 판단하기 위한 방법은 스크롤 이벤트를 사용하는 방법과 `IntersectionObserver를` 사용하는 방법이 있었다.

스크롤 이벤트를 사용하면 스크롤을 할 때마다 이벤트 핸들러가 호출이 되기 때문에 이벤트 핸들러가 너무 많이 호출이 되고 이런 현상을 해결하기 위해서 `debounce`나 `throttle`을 사용해야 했다. 또한 스크롤 이벤트는 현재 높이 값을 알기 위해서 `Element.getBoundingClientRect`을 사용하는데 정확한 값을 가져오기 위해서 매번 layout을 새로 그려서 reflow가 일어나 브라우저 성능이 저해됐다. 반면에 `IntersectionObserver`는 reflow를 발생시키지 않는다. 또한 비동기적으로 이벤트를 발생시키기 때문에 더 효과적이다.

## Intersection Observer
```javascript=
const io = new IntersectionObserver(callback, options) // 관찰자 초기화
io.observe(element) // 관찰할 대상(요소) 등록
```
### callback

target element가 등록되거나 가시성에 변화가 생기면 관찰자는 callback을 실행한다. callback은 두개의 매개변수 `entries`와 `observer`를 가진다.

#### entries
`IntersectionObserverEntry`의 배열이다. 관찰 대상의 교차 영역 정보, 교차 영역 비율, 교차 상태, 관찰 대상 정보, 시간 등의 정보를 포함한다.

`isIntersecting`이 교차하는지를 나타내준다.
![](https://heropy.blog/images/screenshot/intersection-observer/intersection-observer-is-intersecting.jpg)

#### observer
callback이 실행되는 intersectionObserver 인스턴스를 나타낸다.

### options

#### root
target의 가시성을 검사하기 위해서 viewport 대신 사용할 element를 지정한다. 기본값은 브라우저의 viewport이다.

#### rootMargin
`rootMargin`을 사용해서 root의 범위를 확장하거나 축소할 수 있다.

#### threshold
observer가 실행되기 위해서 target의 가시성이 얼마나 필요한지 비율로 나타낸다. 배열로 지정하면 각 요소들에 해당할 때 모두 observer가 실행된다.


## 구현 중 에러
```javascript=
import { useRef, useEffect, useState } from 'react';
import { useSWRInfinite } from 'swr';
import { fetcher } from '../fetchers/fetcher';
import { GET_COMMENTS } from '../graphql/post';

import { mergeList } from '../lib/mergeList';

const useComments = ({ postId, infiniteScrollTargetRef }) => {
  const intersectionObserverRef = useRef();
  const [loadFinished, setLoadFinished] = useState(false);
  const getKey = (pageNum, previousPageData) => {
    console.log('getKey pageNum:', pageNum);
    if (loadFinished) return null;
    if (previousPageData && previousPageData.getComments.length < 3) {
      setLoadFinished(true);
      return null;
    }
    return [GET_COMMENTS, pageNum, postId];
  };
  const { data, error, mutate, size, setSize, isValidating } = useSWRInfinite(
    getKey,
    (query, pageNum, postId) => fetcher(query, { postId, pageNum }),
    {
      revalidateAll: true,
    },
  );
  console.log('size: ', size);

  const flattenData = data ? mergeList(data, 'getComments') : [];
  const isLoadingInitialData = !data && !error;
  const isLoadingMore = isLoadingInitialData || (size > 0 && data && !data[size - 1]);
  useEffect(() => {
    const ioCallback = (entries) => {
      const isIntersecting = entries.some((entry) => entry.isIntersecting);
      if (isIntersecting) {
        console.log(
          'callback called, size: ',
          size,
          'isIntersecting',
          isIntersecting,
          'data:',
          data,
          'error: ',
          error,
          'isValidating: ',
          isValidating,
        );
        setSize(size + 1);
      }
    };
    intersectionObserverRef.current = new IntersectionObserver(ioCallback);
    intersectionObserverRef.current.observe(infiniteScrollTargetRef.current);
  }, [infiniteScrollTargetRef]);

  useEffect(() => {
    if (loadFinished) {
      intersectionObserverRef.current.unobserve(infiniteScrollTargetRef.current);
    }
  }, [loadFinished]);

  return {
    comments: flattenData,
    isLoadingInitialData,
    isLoadingMore,
    isError: error,
    size,
    setSize,
    isValidating,
    mutate,
  };
};

export default useComments;

```

위와 같이 구현을 했는데 계속 두번째 페이지까지만 로딩이 되고 그 이후에 intersecting이 일어날 때마다 계속`ioCallback` 안에서 size가 1인 문제가 있었다. 고민을 해보니 ioCallback이 만들어지는 시점의 size를 참조하고 있어서 이런 문제가 생기는 것 같았다. 따라서 size의 값이 변할 때마다 `ioCallback`에서 다른 size를 참조할 수 있어야했다.

```javascript=
// ...

  const [a, setA] = useState(1);

  // ...
  
  useEffect(() => {
    console.log('recalled, a:', a);
    const ioCallback = (entries) => {
      const isIntersecting = entries.some((entry) => entry.isIntersecting);
      console.log('a: ', a);
      if (isIntersecting) setA(a + 1);
      // setIsIntersecting(isIntersecting);
    };
    if (!intersectionObserverRef.current) {
      console.log('intersection observer set');
      intersectionObserverRef.current = new IntersectionObserver(ioCallback);
      intersectionObserverRef.current.observe(infiniteScrollTargetRef.current);
    }
  }, [infiniteScrollTargetRef, a]);

// ...
```

이렇게 테스트를 해봤는데 실제로 `recalled, a: 2`로 바뀌고 나서도 `ioCallback`안에서 a 값은 그대로 1이었다. a state가 변했지만 new IntersectionObserver를 할 때 매개변수로 들어간`ioCallback`이 참조하는 a states는 이전 상태의 변수였다.

![](https://i.imgur.com/1a4aQ66.png)


그래서 useRef를 사용하는 방법과 state와 useEffect를 사용하는 방법으로 해결을 할 수 있을 것 같은데 useRef를 사용하면

```javascript=
  useEffect(() => {
    const ioCallback = (entries) => {
      const isIntersecting = entries.some((entry) => entry.isIntersecting);
      if (isIntersecting) {
        console.log('size: ', sizeRef.current);
        sizeRef.current += 1;
        setSize(sizeRef.current);
      }
    };
    if (!intersectionObserverRef.current) {
      console.log('intersection observer set');
      intersectionObserverRef.current = new IntersectionObserver(ioCallback);
      intersectionObserverRef.current.observe(infiniteScrollTargetRef.current);
    }
  }, [infiniteScrollTargetRef]);
```

이렇게 만들 수 있을 것 같다. state와 useEffect를 사용하면

```javascript=
  useEffect(() => {
    const ioCallback = (entries) => {
      const isIntersecting = entries.some((entry) => entry.isIntersecting);
      
      setIsIntersecting(isIntersecting);
    };
    if (!intersectionObserverRef.current) {
      console.log('intersection observer set');
      intersectionObserverRef.current = new IntersectionObserver(ioCallback);
      intersectionObserverRef.current.observe(infiniteScrollTargetRef.current);
    }
  }, [infiniteScrollTargetRef]);
  
  // ... 
  
  useEffect(() => {
    console.log('isIntersecting:', isIntersecting);
    if (isIntersecting) {
      console.log(size);
      setSize(size + 1);
    }
  }, [isIntersecting]);
```

이렇게 바꿀 수 있을 것 같다.


최종 코드


```javascript=
const useInfinite = ({ query, variables, fetcher, queryTarget, infiniteScrollTargetRef }) => {
  const intersectionObserverRef = useRef();
  const sizeRef = useRef(1);

  const getKey = (pageNum, previousPageData) => {
    if (previousPageData && previousPageData.getComments.length < 3) {
      intersectionObserverRef.current.unobserve(infiniteScrollTargetRef.current);
      return null;
    }
    return [query, pageNum, ...variables];
  };
  const { data, error, mutate, size, setSize } = useSWRInfinite(getKey, fetcher, {
    revalidateAll: true,
  });

  const flattenData = data ? mergeList(data, queryTarget) : [];
  const isLoadingInitialData = !data && !error;
  const isLoadingMore = isLoadingInitialData || (size > 0 && data && !data[size - 1]);

  useEffect(() => {
    const ioCallback = (entries) => {
      const isIntersecting = entries.some((entry) => entry.isIntersecting);
      if (isIntersecting) {
        console.log('size: ', sizeRef.current);
        sizeRef.current += 1;
        setSize(sizeRef.current);
      }
    };
    if (!intersectionObserverRef.current) {
      intersectionObserverRef.current = new IntersectionObserver(ioCallback);
      intersectionObserverRef.current.observe(infiniteScrollTargetRef.current);
    }
  }, [infiniteScrollTargetRef]);

  return {
    comments: flattenData,
    isLoadingInitialData,
    isLoadingMore,
    isError: error,
    mutate,
  };
};
```


```javascript=
const useComments = ({ postId, infiniteScrollTargetRef }) => {
  const { comments, mutate } = useInfinite({
    query: GET_COMMENTS,
    variables: [postId],
    fetcher: (query, pageNum, postId) => fetcher(query, { postId, pageNum }),
    queryTarget: 'getComments',
    infiniteScrollTargetRef,
  });
  return {
    comments,
    mutate,
  };
};
```


## 참고
https://heropy.blog/2019/10/27/intersection-observer/
https://tech.lezhin.com/2017/07/13/intersectionobserver-overview
https://velog.io/@yejinh/Intersection-Observer로-무한-스크롤-구현하기