[react] Infinite Scroll 구현하기

Suyeon·2021년 2월 11일
36

React

목록 보기
20/26
post-thumbnail

Infinite scrolling is a web-design technique that loads content continuously as the user scrolls down the page, eliminating the need for pagination.

Infinite Scroll을 구현하는 방법은 크게 아래의 세가지로 나뉜다.(라이브러리 제외)
1. Scroll Event
2. IntersectionObserver
3. useRef

Scoll Event

scroll 이벤트는, 세가지 방법중 그닥 추천이 되는 방법은 아니다. 유저의 scrolling에 따라서 이벤트가 굉장히 빈번하게 발생하기 때문에 throttle와 같은 라이브러리를 통한 성능 최적화가 꼭 필요해보인다.


IntersectionObserver

가장 보편적으로 사용되는 방법이 바로 IntersectionObserver인 것 같다. list 요소의 가장 아래에 빈 div을 생성하고, ref을 달아준다. 이 ref를 통해서 교차시점을 확인할 수 있다.

  • (참고) 브라우저의 호환을 위해서 polyfill 필요

useFetch 커스텀 훅 생성

// useFetch.js
import { useState, useEffect, useCallback } from "react";
import axios from "axios";

function useFetch(query, page) {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);
  const [list, setList] = useState([]);

  const sendQuery = useCallback(async () => {
    try {
      await setLoading(true);
      await setError(false);
      const res = await axios.get(url);
      await setList((prev) => [...prev, ...res.data];
      setLoading(false);
    } catch (err) {
      setError(err);
    }
  }, [query, page]);

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

  return { loading, error, list };
}

export default useFetch;

useFetch 커스텀 훅과 함께 infinite scroll 구현

import useFetch from "hooks/useFetch";

function App() {
  const [query, setQuery] = useState("");
  const [page, setPage] = useState(1);
  const { loading, error, list } = useFetch(query, page);
  const loader = useRef(null);

  const handleChange = (e) => {
    setQuery(e.target.value);
  };

  const handleObserver = useCallback((entries) => {
    const target = entries[0];
    if (target.isIntersecting) {
      setPage((prev) => prev + 1);
    }
  }, []);

  useEffect(() => {
    const option = {
      root: null,
      rootMargin: "20px",
      threshold: 0
    };
    const observer = new IntersectionObserver(handleObserver, option);
    if (loader.current) observer.observe(loader.current);
  }, [handleObserver]);

  return (
    <div className="App">
      <h1>Infinite Scroll</h1>
      <h2>with IntersectionObserver</h2>
      <input type="text" value={query} onChange={handleChange} />
      <div>
        {list.map((item, i) => (
          <div key={i}>{item}</div>
        ))}
      </div>
      {loading && <p>Loading...</p>}
      {error && <p>Error!</p>}
      <div ref={loader} />
    </div>
  );
}

export default App;

useRef

유튜브에서 infinite scroll 검색하다가 발견한 useRef를 사용하는 방법이다. (참고 영상) list의 마지막 요소에만 선택적으로 ref를 달아주고, ref가 있을 때, 새롭게 데이터를 fetch한다.

참고
set을 사용하면 중복된 값을 제거할 수 있다.(유니크한 값만 반환)

setBooks((prev) => {
  return [...new Set([...prev, ...res.data)])];
});

useFetch 커스텀 훅 생성

// useFetch.js
import React, { useState, useEffect } from "react";
import axios from "axios";

function useFetch(query, page) {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(false);
  const [list, setList] = useState([]);
  const [hasMore, setHasMore] = useState(false);
  
    const sendQuery = useCallback(async () => {
    try {
      await setLoading(true);
      await setError(false);
      const res = await axios.get(url);
      await setList((prev) => [...new Set([...prev, ...res.data))];
      await setHasMore(res.data.docs.length > 0);
      setLoading(false);
    } catch (err) {
      setError(err);
    }
  }, [query, page]);

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

  return { isLoading, error, books, hasMore };
}

export default useFetch;

useFetch 커스텀 훅과 함께 infinite scroll 구현

import useFetch from "hooks/useFetch";

function App() {
  const [query, setQuery] = useState("");
  const [pageNum, setPageNum] = useState(1);
  const { loading, error, list, hasMore } = useSearchBook(query, pageNum);

  const observer = useRef(); // (*)
  const lastBookElementRef = useCallback(  // (*)
    (node) => {
      if (isLoading) return;
      if (observer.current) observer.current.disconnect();
      observer.current = new IntersectionObserver((entries) => {
        if (entries[0].isIntersecting && hasMore) {
          setPageNum((prev) => prev + 1);
        }
      });
      if (node) observer.current.observe(node);
    },
    [loading, hasMore]
  );

  const handleChange = (e) => {
    setQuery(e.target.value);
    setPageNum(1);
  };
  
  return (
    <div className="App">
      <h1>Infinite Scroll</h1>
      <h2>with useRef</h2>
      <input type="text" onChange={handleChange} value={query} />
      {list.map((item, i) => {
        const isLastElement = books.length === i + 1;
        isLastElement ? (
          <div key={i} ref={lastBookElementRef}> 
          {book}
          </div>
        ) : (
        <div key={i}>{book}</div>
        )
      })}
      <div>{isLoading && "Loading..."}</div>
      <div>{error && "Error..."}</div>
    </div>
  );

export default App;
profile
Hello World.

3개의 댓글

comment-user-thumbnail
2021년 9월 17일

잘봤습니다!

1개의 답글
comment-user-thumbnail
알 수 없음
2021년 9월 22일
수정삭제

삭제된 댓글입니다.

1개의 답글