상세 컨텐츠

본문 제목

[React] Infinite scroll (1) - React query

React

by 일단두잇 2023. 3. 8. 16:02

본문

반응형

Infinite scroll은 사용자가 페이지 하단에 도달했을 때, 콘텐츠가 계속 로드되는 리스트입니다.사용자는 페이지 이동없이 Scroll을 하게되고 Scroll에 따라 컨텐츠가 로드 됨으로써 지속적인 컨텐츠를 보여주게 됩니다. 특히 모바일에서 많이 사용합니다.

 

구현 방법

  1. IntersectionObserver 사용
  2. Event에서 페이지 번호 증가
  3. 추가 컨테츠 로딩

 

Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document 의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법입니다.

 

  • IntersectionObserver를 정의합니다.
  • clean-up 함수를 정의하여 해제합니다.
const observer = useRef<IntersectionObserver>();
useEffect(() => {
if (!observer.current) {
observer.current = new IntersectionObserver(handleObserver, { ...defaultOptions });
const current = loader.current;
current && observer.current.observe(current);
return () => {
current && observer.current?.unobserve(current);
};
}
}, [hasMore]);
 

2. Event에서 페이지 번호 증가

  • 페이지 하단에 도달했을때 Event를 호출하고 페이지 번호를 증가해 줍니다.
const handleObserver = ([target]: IntersectionObserverEntry[]) => {
if (target.isIntersecting && hasMore) {
page.current = page.current + 1;
next(page.current);
}
};
 

3. 추가 컨텐츠 로드

  • 데이터 caching을 위해 React Query를 사용합니다.
  • key를 page 번호로 설정하여 해당 쿼리를 재호출 합니다.
  • 결과 데이터를 리스트 데이터에 추가합니다.
  • totalPage에 도달하면 추가 컨텐츠 로드를 멈춤니다.
const { isLoading, isRefetching, data } = useQuery(['getTodoGroupList', page], () => getTodoGroupList(page), {
enabled: page > 0,
});
useEffect(() => {
if (!isRefetching && !isLoading && page > 0 && data) {
setItems([...items, ...data]);
if (page > totalPage || data.length === 0) {
setHasMore(false);
}
}
}, [isLoading, isRefetching]);
 
 

최종 Code

 

HomePage.tsx

export default function HomePage() {
const navigate = useNavigate();
const totalPage = 2 // To-do : server data;
const root = useRef<HTMLDivElement>(null);
const [items, setItems] = useState<ITodoGroup[]>([]);
const [page, setPage] = useState(0);
const [hasMore, setHasMore] = useState(true);
const { isLoading, isRefetching, data } = useQuery(['getTodoGroupList', page], () => getTodoGroupList(page), {
enabled: page > 0,
});
useEffect(() => {
if (!isRefetching && !isLoading && page > 0 && data) {
setItems([...items, ...data]);
if (page > totalPage || data.length === 0) {
setHasMore(false);
}
}
}, [isLoading, isRefetching]);
const Item = ({ item }: { item: ITodoGroup }) => {
return (
<TodoGroupItem
data-testid="todo-group-item"
key={item.todoGroupId}
item={item}
onClick={() => navigate(`/todo/${item.todoGroupId}`, { state: item })}
/>
);
};
return (
<PageContainer>
<TitleContainer>
<Title>Home Page</Title>
</TitleContainer>
<PageContent ref={root}>
<StyledList>
<InfiniteScroll next={(page: number) => setPage(page)} hasMore={hasMore}>
<>
{items.map((item) => {
return <Item key={item.todoGroupId} item={item} />;
})}
</>
</InfiniteScroll>
</StyledList>
</PageContent>
</PageContainer>
);
}
const StyledList = styled(VerticalBox)`
padding: 10px 15px;
`;
 
import React, { ReactElement, useEffect, useRef } from 'react';
const defaultOptions = {
root: null,
rootMargin: '0px',
threshold: 1.0,
};
interface IInfiniteScroll {
children: ReactElement;
root?: Document | null;
next: (_page: number) => void;
hasMore: boolean;
}
export default function InfiniteScroll({ children, next, hasMore }: IInfiniteScroll) {
const loader = useRef(null);
const observer = useRef<IntersectionObserver>();
const page = useRef(0);
const oldPage = useRef(0);
useEffect(() => {
const handleObserver = ([target]: IntersectionObserverEntry[]) => {
if (target.isIntersecting && hasMore) {
page.current = page.current + 1;
oldPage.current = page.current;
next(page.current);
}
};
if (!observer.current) {
observer.current = new IntersectionObserver(handleObserver, { ...defaultOptions });
const current = loader.current;
current && observer.current.observe(current);
return () => {
current && observer.current?.unobserve(current);
};
}
// eslint-disable-next-line
}, [hasMore]);
return (
<>
{children}
<div ref={loader} />
</>
);
}
 

 

 

반응형

'React' 카테고리의 다른 글

관련글 더보기

댓글 영역