리액트 Infinite scroll 구현

Gunwoo Kim·2021년 7월 27일
1

React

목록 보기
15/21
post-thumbnail

Infinite scroll

Infinite scroll이란 무한 스크롤로 아래와 같이 스크롤이 최대치(콘텐츠의 끝)까지 내려가면 자바스크립트 코드에 의해 새로운 콘텐츠가 생성 및 추가되는 방식입니다.

이미지

이번에는 무한 스크롤을 구현해보도록 하겠습니다.

1. scroll 이벤트

첫번째로 구현한 방식은 scroll 이벤트를 통한 구현입니다.

우선 요청하는 서버의 API 형식입니다.

위 와 같이 page 와 limit을 parameter로 서버에 요청하여 page는 현재 요청하고자 하는 데이터의 위치라고 보면 되고 limit은 현재 요청한 데이터의 갯수라고 생각하시면 됩니다.

_page=1&_limit=10의 경우 1번항목부터 10번 항목까지 10개의 항목을 받아오고 _page=2&_limit=10 의 경우 11번항목부터 20번까지 10개의 항목을 순서대로 받습니다.

이런식으로 10개의 항목씩 순차적으로 받아와서 기존 항목에 추가하는 방식입니다.

const [page, setPage] = useState(1);
const [items, setItems] = useState([]);

const getFetchData = () => {
    const url = `{API}/comments?_page=${page}&_limit=10`;
    fetch(url)
      .then((res) => res.json())
      .then((item) => setItems((prev) => [...prev, ...item]));
  };

useEffect(() => getFetchData(), [page]);

10개씩 받아오는 항목은 고정이라 하고 limit값은 10으로 고정하겠습니다.

이때 page의 값은 변경이 되기 때문에 page와 받아오는 items 이렇게 2가지를 useState로 관리하고 fetch 를 통한 데이터는 기존 데이터와 새롭게 받아온 데이터로 업데이트 해주는 getFetchData 함수를 만들어 주었습니다.

이때 패치는 page의 변화에 따라 호출하기 위해 useEffect를 통해 getFetchData 함수를 호출하도록 하였습니다.


  useEffect(() => {
    window.addEventListener("scroll", onScroll);
    return () => window.removeEventListener("scroll", onScroll);
  }, []);

  const onScroll = () => {
    const scrollTop = document.documentElement.scrollTop;
    const clientHeight = document.documentElement.clientHeight;
    const scrollHeight = document.documentElement.scrollHeight;

    if (scrollTop + clientHeight === scrollHeight) {
      setPage((prev) => prev + 1);
    }
  };

이번에 볼 부분은 scroll 이벤트를 주는 부분으로 초기 렌더가 될때 위 와 같이 scroll 이벤트를 추가하고 마운트 될때는 해당 이벤트가 더 이상 동작하지 않도록 제거해줍니다.

onScroll 함수를 통해 현재 스크롤의 높이와 스크롤의 높이와 현재 보고있는 화면의 높이가 같으면 컨텐츠의 제일 끝으로 판단하여 현재 페이지에 +1을 해주게 되면 page 변경을 감지한 useEffect가 getFetchData 함수를 호출하여 값을 조회해옵니다.

// InfiniteScrollList.js
import { useState, useEffect } from "react";
import CommentItem from "components/CommentItem";

const InfiniteScrollList = () => {
  const [page, setPage] = useState(1);
  const [items, setItems] = useState([]);

  const getFetchData = () => {
    const url = `{API}?_page=${page}&_limit=10`;
    fetch(url)
      .then((res) => res.json())
      .then((item) => setItems((prev) => [...prev, ...item]));
  };

  useEffect(() => getFetchData(), [page]);

  useEffect(() => {
    window.addEventListener("scroll", onScroll);
    return () => window.removeEventListener("scroll", onScroll);
  }, []);

  const onScroll = () => {
    const scrollTop = document.documentElement.scrollTop;
    const clientHeight = document.documentElement.clientHeight;
    const scrollHeight = document.documentElement.scrollHeight;

    if (scrollTop + clientHeight === scrollHeight) {
      setPage((prev) => prev + 1);
    }
  };

  return (
    <div>
      {items?.map((item) => (
        <CommentItem key={item.id} item={item} />
      ))}
    </div>
  );
};

export default InfiniteScrollList;

위는 최종 InfiniteScrollList.js 컴포넌트입니다.

2. Intersection Observer API

두번째 방법은 Intersection Observer라는 브라우저가 제공하는 API를 사용하는 방법입니다.

위와 같이 첫번째 방법의 경우 scroll 이벤트가 너무 많이 발생하여 성능에 영향을 줄 수 있다고 합니다.(이 부분은 아직 경험해보지 못했습니다.)

👍 Intersection Observer

Intersection Observer API는 다음과 같은 상황에 호출되는 콜백을 생성하는 기능을 제공합니다:

(1) 대상(target) 으로 칭하는 요소가 기기 뷰포트나 특정 요소(이 API에서 이를 root 요소 혹은 root로 칭함)와 교차함.

(2) observer가 최초로 타겟을 관측하도록 요청받을 때마다.

import { useRef, useState, useEffect } from "react";
import CommentItem from "components/CommentItem";

const InfiniteObserverList = () => {
  const [page, setPage] = useState(0);
  const [items, setItems] = useState([]);
  const observer = useRef();

  const getFetchData = () => {
    const url = `{api}/comments?_page=${page}&_limit=10`;
    fetch(url)
      .then((res) => res.json())
      .then((item) => setItems((prev) => [...prev, ...item]));
  };

  useEffect(() => page !== 0 && getFetchData(), [page]);

  const onIntersect = (entries) => {
    const target = entries[0];
    if (target.isIntersecting) setPage((p) => p + 1);
  };

  useEffect(() => {
    if (!observer.current) return;

    const io = new IntersectionObserver(onIntersect, { threshold: 1 });
    io.observe(observer.current);

    return () => io && io.disconnect();
  }, [observer]);

  return (
    <div>
      {items?.map((item) => (
        <CommentItem key={item.id} item={item} />
      ))}
      <div ref={observer} />
    </div>
  );
};

export default InfiniteObserverList;

기존 scroll 이벤트 와는 다르게 대상(target)의 뷰포트나 특정요소의 교차 여부를 알아야하기 때문에 컴포넌트 내부에 가장 끝에 감지할 요소를 추가하고 해당 요소의 정보를 알기위해 observer 라는 useRef를 선언해줍니다.

useEffect로 요소가 있는지 확인하고 있을 경우 page값을 변경합니다.

IntersectionObserver 의 구조
let observer = new IntersectionObserver(callback, options);

IntersectionObserver의 경우 위 와같이 구조가 되어있고 첫번째 인자에는 뷰포트가 겹칠 경우 호출할 callback 함수를 두번째 인자에는 옵져버를 조정할 수 있는 옵션 값입니다.

더 자세한 정보는 👉 Intersection Observer - 요소의 가시성 관찰

IntersectionObserver의 콜백 함수는 콜백은 2개의 인수(entries, observer)를 가집니다.

entries는 IntersectionObserverEntry 인스턴스의 배열이고 우리는 첫번째 요소의 관찰 대상의 교차 상태 정보가 필요하기 때문에 아래와 같이 타겟의 교차상태일때 변경해주도록 합니다.

const target = entries[0]; 
if (target.isIntersecting) setPage((p) => p + 1);

훅의 경우 useEffect를 초기에 한번 실행시키기 때문에 getFetchData 함수를 통해 데이터롤 요청하게 됩니다.

그리고 또 다른 useEffect를 통해 IntersectionObserver를 통해 초기 뷰포트가 겹치기 때문에 setPage가 일어나 한번 더 호출하여 2중으로 호출하게 되기 때문에 초기 값을 0으로 주고 0일 경우에는 호출되지 않도록 했고 IntersectionObserver를 통해 page가 1로 변경되면서 첫번째 항복을 조회하게 됩니다.

👉 코드 구현 GitHub 주소
👉 하얀마인드 API 제공 👍👍👍

0개의 댓글