ETC -

React 무한 스크롤(Offset-based)

  • -

React 무한 스크롤(Offset-based)

안녕하세요 TriplexLab 입니다.

오늘은 Intersection Observer API를 활용해서 React 무한 스크롤(Offset-based)에 관해서 이야기를 하겠습니다.
(다음 내용은 프론트 입장에서 무한 스크롤기능을 구현하는데 초점을 둔 내용입니다.)

코드를 보기 앞서 무한 스크롤에는 크게 2가지 방법이 있습니다.
Offset-based, Cursor-based 입니다.

👉🏻 Offset-based

📌 장점

  • offset, limit 을 사용한 쿼리 이용 (MySQL 기준)
  • 페이지 단위로 구분
  • 직관적이고 구현도 간단합니다.

📌 단점

  • 데이터 중복 문제(사용자가 동시에 컨텐츠를 생성 또는 삭제을 할경우에 중복)
  • 성능 저하 문제

성능 저하 문제는 둘째 치더라도, 데이터 중복문제는 해결할 수 없습니다. 
Cursor-based을 사용하면 위 문제점들을 모두 해결할 수 있습니다.

Offset-based, Cursor-based에 대해서 자세한 설명은 구글링하면 많이 나오는 내용입니다.
이번 포스트에서는 Offset-based으로 실제 구현하는 내용만 공유하도록 하겠습니다.
(Cursor-based 이야기는 나중에 다른 포스트에서 공유하겠습니다.)

👉🏻 GET요청 API 

// src/api.js

export async function getReviews({
  order = 'createdAt',
  offset = 0,
  limit = 6,
}) {
  const query = `order=${order}&offset=${offset}&limit=${limit}`;
  const response = await fetch(
    `https://learn.codeit.kr/api/film-reviews?${query}`
  );
  const body = await response.json();
  return body;
}

👉🏻 무한스크롤에 해당된 컴포넌트

(저는 간단한 DEMO버전이기때문에 APP.js에서 무한스크롤기능을 만들었습니다.
필요하면 다른 컴포넌트를 만들어서 해도 무방 합니다. 😃😃)

// src/components/App.js

import { useEffect, useState } from 'react';
import ReviewList from './ReviewList';
import { getReviews } from '../api';

const LIMIT = 6;

function App() {
const [order, setOrder] = useState('createdAt');
  const [offset, setOffset] = useState(0);
  const [hasNext, setHasNext] = useState(true);
  const [items, setItems] = useState([]);
  const [target, setTarget] = useState(null); // 구독할 대상 (target을 지켜보고 있다가 이 target이 정해진 threshold 비율만큼 보이면 지정한 행동을 합니다. )
  
  const handleLoad = async (options) => {
    const { paging, reviews } = await getReviews(options);
    if (options.offset === 0) {
      setItems(reviews);
    } else {
      setItems([...items, ...reviews]);
    }
    setOffset(options.offset + options.limit);
    setHasNext(paging.hasNext);
  };
  
  useEffect(() => {
    let options = {
      threshold: "1", //타겟 엘리먼트가 교차영역에 진입했을 시점에 observer를 실행하는 것을 의미
    };

    // 새롭게 생성할 observer가 수행할 행동 정의
    let handleIntersection = async ([entries], observer) => {
      if (entries.isIntersecting) {
        hasNext && await handleLoad({ order, offset: offset, limit: LIMIT });
        observer.unobserve(entries.target);
      }
    };

    // 새로운 observer 생성
    const io = new IntersectionObserver(handleIntersection, options);
    if (target) io.observe(target);

    offset === 0 && handleLoad({ order, offset: 0, limit: LIMIT })
    return () => io && io.disconnect();
    
  }, [target, offset]);
  
  return (
    <div>
      <ReviewList items={sortedItems} setTarget={setTarget} />
    </div>
  );
}

export default App;

아래 코드에 의거해서 target설정을 가장 마직막의 DOM을 받아올수 있습니다.
const lastItem = idx === items.length - 1;

// src/components/ReviewList.js

function ReviewList({ items, onDelete, setTarget }) {
  return (
    <ul>
      {items.map((item, idx) => {
        //* 새로 불어온 데이터 중 가장 마지막 값을 찾아 target으로 설정함 (마지막 데이터를 구독, 데이터를 새로 불러올 때마다 target이 바뀜) */
        const lastItem = idx === items.length - 1;
        return (
          <li key={idx}>
            <ReviewListItem item={item} onDelete={onDelete} ref={lastItem ? setTarget : null} /> // setTarget을 마지막 item일때 props로 전달
          </li>
        );
      })}
    </ul>
  );
}
// src/components/ReviewList.js

const ReviewListItem = forwardRef(({ item, onDelete }, ref) => { //forwardRef를 사용해서 ref를 받는다.
  const handleDeleteClick = () => {
    onDelete(item.id);
  };

  return (
    <div className="ReviewListItem" ref={ref}> // 위에서 ref를 받아온것을 적용합니다.
      <img className="ReviewListItem-img" src={item.imgUrl} alt={item.title} />
      <div>
        <h1>{item.title}</h1>
        <p>{item.rating}</p>
        <p>{formatDate(item.createdAt)}</p>
        <p>{item.content}</p>
        <button onClick={handleDeleteClick}>삭제</button>
      </div>
    </div>
  );
})

👉🏻정리

Cursor-based Pagination이 필수는 아닙니다.
아래와 같은 조건을 모두 만족한다면 Offset-based 을 사용해도 전혀 문제가 없습니다.

1. 중복데이터가 발생해도 상관 없는 경우
2. 전체 데이터 양이 적은 경우
3. 새로운 데이터 생성이 빈번하지 않은 경우
4. 검색엔진이 색인을 생성하지 않고, 임의의 사용자가 오래된 데이터를 조회하지 않는 경우

Cursor-based를 구현해야 하는 경우 백엔드에서 조금 더 공수가 들어간다.
정말 간단한 백오피스(관리자 페이지가 따로 있는 서비스) 성격의 서비스라면 Offset-based 방식이 나을 것 같다.
하지만 그 이외에 경우에는 처음부터 Cursor-based Pagination 방식으로 구현하는게 좋은 것 같다.

🧑🏻‍💻 전체 코드 공유

 

GitHub - younhoso/younhoso

Contribute to younhoso/younhoso development by creating an account on GitHub.

github.com

👏🏻 Live Demo

 

MOVIE PEDIA

 

younhoso.github.io

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.