Language/React

InfiniteScroll 컴포넌트 간단구현

건담아빠 2025. 2. 24. 11:36

react + java + jpa 환경에서 InfiniteScroll을 간단하게 구현해 보자.

 

React

  • views Component
const ReceiptViews: React.FC<ReceiptViewsProps> = (props) => {
  const [loading, setLoading] = useState<boolean>(false);
  const [hasMore, setHasMore] = useState<boolean>(true);
  const paginationRef = useRef<PaginationType>({ current: 1, pageSize: 9 });
  const pageNoRef = useRef<number>(1);
  ...
    
  const fetchItems = async () => {
    setLoading(true);

    const l_pageNo = pageNoRef.current++;
    const l_filters = { ...filtersRef.current };
    l_filters.receiptYm = props.receiptYm;

    const params = {
      pageNo: l_pageNo,
      pageSize: paginationRef.current.pageSize,
      ...l_filters,
    };

    const response: ApiReceiptListData = await request.get('/v2/.../receipts', params, { isJson: false });
    if (response.status === 200) {
      if (l_pageNo === 1) {
        setReceipts(response.items);
      } else {
        // 기존 receipts 배열에 새로운 데이터 추가
        setReceipts((prev) => [...prev, ...response.items]);
      }

      setHasMore(response.hasMore);
    }

    setLoading(false);
  };
  ...
  
  return (
    <>
      <div className="inner-content box-list-wrap">
        ...
        <>
          {receipts.length === 0 ? (
            <div className="no-data receipt-data-table-empty">
              <img src={require('images/image_empty_box.svg').default} alt="" />
              <div className="title">해당하는 데이터가 없습니다.</div>
            </div>
          ) : (
            <div className="items-wrap">
              <InfiniteScroll
                items={receipts}
                loading={loading}
                hasMore={hasMore}
                onLoadMore={fetchItems}
                loadingComponent={<div className="loading-spinner">Loading more receipts...</div>}
              >
                <div className="items-grid">
                  {receipts.map((item) => (
                    <ReceiptViewCard
                      receipt={item}
                      onChangeStatus={(l_receipt: ReceiptListItemData) => {
                        handleChangeStatus(l_receipt);
                      }}
                      onReceiptViewPop={(l_receipt: ReceiptListItemData) => {
                        setIsOpenReceiptViewPop(true);
                        setReceipt(l_receipt);
                      }}
                    />
                  ))}
                </div>
              </InfiniteScroll>
            </div>
          )}
          ...
        </>
      </div>
    </>
  );
};

export default ReceiptViews;

 

  • InfiniteScroll Component

IntersectionObserver API를 사용하여 스크롤 감지하고 스크롤이 특정 지점에 도달하면 onLoadMore함수 실행을 통하여 추가 데이터를 로드하고 로딩 중일 때 로딩 컴포넌트를 표시한다.

import React, { useEffect, useRef, useCallback } from 'react';

interface InfiniteScrollProps<T> {
  items: T[];
  loading: boolean;
  hasMore: boolean;
  onLoadMore: () => Promise<void>;
  children: React.ReactNode;
  threshold?: number;
  loadingComponent?: React.ReactNode;
}

function InfiniteScroll<T>({
  items,
  loading,
  hasMore,
  onLoadMore,
  children,
  threshold = 0.8,
  loadingComponent = <div>Loading...</div>,
}: InfiniteScrollProps<T>) {
  const observerRef = useRef<IntersectionObserver | null>(null);
  const loadingRef = useRef<HTMLDivElement>(null);

  const handleObserver = useCallback(
    (entries: IntersectionObserverEntry[]) => {
      const target = entries[0];
      if (target.isIntersecting && hasMore && !loading) {
        onLoadMore();
      }
    },
    [hasMore, loading, onLoadMore]
  );

  useEffect(() => {
    const currentLoadingRef = loadingRef.current;

    if (currentLoadingRef) {
      observerRef.current = new IntersectionObserver(handleObserver, {
        root: null,
        rootMargin: '20px',
        threshold: threshold,
      });

      observerRef.current.observe(currentLoadingRef);
    }

    return () => {
      if (observerRef.current && currentLoadingRef) {
        observerRef.current.unobserve(currentLoadingRef);
      }
    };
  }, [handleObserver, threshold]);

  return (
    <div className="infinite-scroll-container">
      {children}
      {(hasMore || loading) && (
        <div ref={loadingRef} className="loading-trigger">
          {loading && loadingComponent}
        </div>
      )}
    </div>

    // <>{children}</>
  );
}

export default InfiniteScroll;

 

java + jpa

  • ApiController

haMore로 이후 페이지를 확인한다.

...
public class ReceiptController extends BaseController {
  private final ReceiptService receiptService;

  @Operation(summary = "수납 내역", description = "")
  @GetMapping(value = "/receipts")
  public ResponseEntity<ResponseDto> getList(@ModelAttribute @Valid ReceiptRequestDto.ReceiptListRequest listRequestDto) {
    ...
    Page<BaseDto> contentPage = receiptService.getListPage(listRequestDto, memberDTO.getID());

    ResponseDto res = ResponseListDto.builder()
        .status(HttpStatus.OK.value())
        .message("성공적으로 조회했습니다.")
        .items(contentPage.getContent())
        .totalElements(contentPage.getTotalElements())
        .hasMore(Utils.hasMorePageable(contentPage))
        .build();

    return new ResponseEntity<>(res, HttpStatus.OK);
  }
}

 

  • Utils
public class Utils {
  ...

  public static boolean hasMorePageable(Page<BaseDto> contentPage) {
    long totalElements = contentPage.getTotalElements();
    int pageSize = contentPage.getPageable().getPageSize();
    int pageNumber = contentPage.getPageable().getPageNumber();

    return ((long) pageSize * (pageNumber + 1)) < totalElements;
  }
}