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;
}
}