React 무한스크롤 (Feat: Intersection Observer)

5_wintaek·2023년 8월 8일
0

들어가며😃

프리온보딩 인턴쉽 과제를 하던 도중, 무한스크롤 기능을 넣으라는 조건이 있었는데 당시 제출기한이 너무 짧았고,무한스크롤의 대한 이해도와 코드가 어려운 탓에기능을 완전하게 끝내지 못하고 제출하였기 때문에 리펙토링을 하면서 이 글을 쓰게 되었다! Github API를 활용하여 특정 레파지토리에 올라온 IssueList들을 뽑아서 구현하는 것이였다. 1000개가 넘는 IssueList들을 화면에 리스트를 렌더링 해야 하는데 이것을 페이지네이션이 아닌 무한스크롤로 구현을 해야 했다.

무한스크롤이란 ? 🤔

게시판 글 리스트처럼 많은 데이터를 배열로 받아오는 경우, 데이터가 너무 많아지면 API 요청으로 데이터를 받아오는 시간이 오래걸릴 수 밖에 없다.무한스크롤은 한 페이지의 스크롤의 바닥에 도달할 때 api를 요청하는 방식이다. 그렇다면 어떻게 스크롤을 바닥에 도달했는지? 를 알아야 한다. 대표적으로 ScrollEvent방식과 Intersection Observer API를 활용하는 방식이 있다. 나는 usehook으로 관리를 하여 깔끔하게 분리를 하고 싶어 Intersection Observer API를 채택하였다.

💡 Intersection Observer API

Intersection Observer API
Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document 의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법입니다.
출처 : MDN

스크롤을 내리다 보면 뷰포트에 타겟 요소가 들어오게 되면 데이터를 불러온다. 불러운 데이터는 기존 데이터와 타겟 요소 사이에 추가한다. 이런식으로 계속 반복되는 방법이다.

new IntersectionObserver()를 통해 생성한 인스턴스(io)로 관찰자(Observer)를 초기화하고 관찰할 대상(Element)을 지정한다.
생성자는 2개의 인수(callback, options)를 가진다.

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

callback

관찰할 대상(Target)이 등록되거나 가시성(Visibility, 보이는지 보이지 않는지)에 변화가 생기면 관찰자는 콜백(Callback)을 실행한다.
콜백은 2개의 인수(entries, observer)를 가진다

const io = new IntersectionObserver((entries, observer) => {}, options)
io.observe(element)

entries

enrty는 IntersectionObserver에 의해 반환되는 관찰 대상 요소의 정보를 나타낸다. IntersectionObserver는 요소의 가시성에 따라 콜백 함수를 호출하고, 해당 콜백 함수의 매개변수로 entry 배열을 전달한다. 여기서 entry는 IntersectionObserverEntry 객체이다. 이 객체는 관찰 대상 요소와 뷰포트(화면) 사이의 교차 정보를 제공한다.

isIntersecting: 이 속성은 관찰 대상 요소가 현재 뷰포트 안에 보이는지 여부를 나타내는 불리언 값입니다. true인 경우 요소가 뷰포트 안에 있으며, false인 경우 요소가 뷰포트 밖에 있다.

📚 Intersection Observer API 를 사용하여 구현하기

나는 useInfiniteScroll 이라는 유틸함수를 만들어서 꺼내서 사용을 할 것이다.

const useInfiniteScroll = (target) => {
  const [intersecting, setIntersecting] = useState(false);
  const observerRef = useRef(null);

  const getObserver = useCallback(() => {
    if (!observerRef.current) {
      observerRef.current = new IntersectionObserver((entries) =>
        setIntersecting(entries.some((entry) => entry.isIntersecting))
      );
    }
    return observerRef.current;
  }, 

  useEffect(() => {
    const observer = getObserver();

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

    return () => {
      observer.disconnect();
    };
  }, [getObserver, target]);

  return intersecting;
};
  1. useInfiniteScroll 은 'target' 이라는 매개변수를 받는다. target은 무한 스크롤을 감지할 대상 요소를 가리키는 ref 이다. 이 요소가 화면에 보일 떄 무한 스크롤 작동을 감지하게 된다.

  2. const [intersecting, setIntersecting] = useState(false);: intersecting이라는 상태 변수와 이를 변경할 setIntersecting 함수를 생성한다. 이 변수는 현재 대상 요소와의 교차 상태를 나타낸다.

  3. const observerRef = useRef(null) 에서 observerRef는 IntersectionObserver 인스턴스를 보관하기 위한 ref 이다. 이렇게 하는 이유는 getObserver함수 내에서 새로운 인스턴스를 생성하고 재사용하기 위해서 사용되었다.

  4. useCallback을 하는 이유는 getObserver 함수를 최적화 하기 위해서 이다. 함수 내부에서 생성하는 IntersectionObserver 인스턴스는 컴포넌트가 렌더링될 떄마다 새로 생성되지 않도록 해야 한다. 이를 위해 useCallback을 사용하여 함수를 메모이제이션하고 동일한 인스턴스를 재사용하기 위해 사용하였다.

  5. if (!observerRef.current) 이 부분이 이해가 제일 가지 않았다. 그래도 구글링 끝의 결국 알아냈다🥹 observerRef.current는 현재의 IntersectionObserver 인스턴스를 가리키는 참조이다. 즉!observerRef.current은 observerRef.current에 아직 인스턴스가 할당되지 않았을 때를 체크하고 있는 부분이다.

    IntersectionObserver는 무한 스크롤과 같이 화면에 특정 요소가 보일 때 작동하는 기능을 구현할 때 사용한다. 이때 IntersectionObserver 인스턴스가 이미 만들어져 있는지를 확인하고, 없다면 새로 만들어야 한다.

    observerRef.current에는 현재 사용 중인 IntersectionObserver 인스턴스가 저장된다. 따라서 if (!observerRef.current)는 "현재 사용 중인 IntersectionObserver 인스턴스가 없으면"을 의미한다. 즉, 해당 인스턴스가 아직 만들어지지 않았을 때 아래의 코드 블록을 실행하게 된다.

if (!observerRef.current) {
  observerRef.current = new IntersectionObserver((entries) =>
    setIntersecting(entries.some((entry) => entry.isIntersecting))
  );
}

화면에 특정 요소가 보일 때 setIntersecting함수를 호출하여 intersecting 상태를 업데이트하는 역할을 한다. 결국 이 부분의 목적은 한 번만 IntersectionObserver 인스턴스를 생성하고, 이후에는 인스턴스를 재사용하여 불필요한 인스턴스 생성을 방지하고 메모리와 성능을 개선하는 것.

  1. useEffect 훅은 컴포넌트가 마운트되거나 target 이 변경될 떄마다 작동한다. 여기서 IntersectionObserver를 설정하고 대상 요소를 감시한다. Observe 메서드를 사용하여 대상 요소의 가시성 변화를 감지하게 된다.

📝 데이터 함수 생성

 const fetchIssueList = async () => {
    setIsLoading(true);
    try {
      const currentPage = issueListPage;
      const data = await getIssueList(10, currentPage);
      setIssueListPage((prev) => prev + 1);
      setIssueList((prevList) => [...prevList, ...data]);
    } catch (error) {
      setFetchError(error);
    } finally {
      setIsLoading(false);
    }
  };
  1. setIssueList((prevList) => [...prevData, ...newData]);: 이 부분은 데이터를 업데이트하는 부분이다. prevData는 이전의 데이터 배열을 의미하고, newData는 새로 가져온 데이터 배열을 의미한다. 기존의 데이터 배열 prevData와 새로 가져온 데이터 배열 newData를 합쳐서 새로운 데이터 배열을 생성하고, setData 함수를 사용하여 컴포넌트의 상태를 업데이트한다. 이렇게 함으로써 새로운 데이터가 기존 데이터 배열에 계속해서 추가된다.

  2. setIssueListPage((prev) => prevPage + 1);: 이 부분은 페이지를 업데이트하는 부분이다. prevPage는 이전의 페이지 숫자를 의미한다. setPage 함수를 사용하여 현재 페이지 숫자를 이전 페이지 숫자에 1을 더한 값으로 업데이트한다. 이렇게 함으로써 다음에 가져올 데이터는 다음 페이지의 데이터를 가져오게 된다.

끝내며

과제를 수행하며 무한스크롤 부분이 제일 난감했던 부분이였다. 아무리 찾아봐도 코드를 어떻게 적용해야 할지를 몰랐고, 심지어 코드를 찾아보았을떄도 이해를 하지 못했던 부분이 제일 컸다. 그래서 글의 중점 내용들을 코드를 해석하고 이해하는 것에 중점을 두었다. 이 글을 작성하면서 까지도 계속 혼자서 되뇌이고 다시한번 찾아보고 하였다. 나중에 내가 찾아봤을때 다시 이해가 갈 수 있게끔 나만의 방식으로 정리를 한거라 다른분들이 보기에는 이해하기가 어려울 수 있으니 참고 사이트를 남기겠다! 😄

출처 :문가네 개발 블로그, HEROPY, sjoLeee

profile
물음표를 느낌표로 바꾸는 개발자

1개의 댓글

comment-user-thumbnail
2023년 8월 8일

공감하며 읽었습니다. 좋은 글 감사드립니다.

답글 달기