Infinite scroll은 사용자가 페이지 하단에 도달했을 때, 콘텐츠가 계속 로드되는 리스트입니다.사용자는 페이지 이동없이 Scroll을 하게되고 Scroll에 따라 컨텐츠가 로드 됨으로써 지속적인 컨텐츠를 보여주게 됩니다. 특히 모바일에서 많이 사용합니다.
구현 방법
Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document 의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법입니다.
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에서 페이지 번호 증가
const handleObserver = ([target]: IntersectionObserverEntry[]) => { if (target.isIntersecting && hasMore) { page.current = page.current + 1; next(page.current); } };
3. 추가 컨텐츠 로드
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] Function Component (0) | 2024.01.12 |
---|---|
[React] Infinite scroll(2) - React query + Recoil + 최적화 (0) | 2023.03.08 |
[React] Recoil Selector (0) | 2023.03.01 |
React Query - useQuery 맛보기 (0) | 2023.03.01 |
React 조건부 Rendering시 주의점 (0) | 2023.03.01 |
댓글 영역