ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • InfiniteScroll 컴포넌트 간단구현
    Language/React 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;
      }
    }

     

     

    댓글

Designed by Tistory.