import React, { useEffect, useRef } from 'react';
import { useInfiniteQuery, QueryFunctionContext } from '@tanstack/react-query';
interface Photo {
    id: number;
    title: string;
    url: string;
    thumbnailUrl: string;
}
const fetchPhotos = async ({ pageParam = 1 }: QueryFunctionContext): Promise<Photo[]> => {
    const res = await fetch(`https://jsonplaceholder.typicode.com/photos?_page=${pageParam}&_limit=10`);
    return res.json();
};
const InfiniteScroll: React.FC = () => {
    const { data, error, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery<Photo[], Error>({
        queryKey: ['photos'],
        queryFn: fetchPhotos,
        getNextPageParam: (lastPage, allPages) => lastPage.length ? allPages.length + 1 : undefined,
    });
    const observerRef = useRef<HTMLDivElement | null>(null);
    useEffect(() => {
        const observer = new IntersectionObserver((entries) => {
            if (entries[0].isIntersecting && hasNextPage) {
                fetchNextPage();
            }
        });
        if (observerRef.current) {
            observer.observe(observerRef.current);
        }
        return () => {
            if (observerRef.current) {
                observer.unobserve(observerRef.current);
            }
        };
    }, [fetchNextPage, hasNextPage]);
    if (error) return <div>Failed to load</div>;
    return (
        <div>
            <h1>Infinite Scroll</h1>
            <div>
                {data?.pages.map((page, i) => (
                    <React.Fragment key={i}>
                        {page.map((photo: Photo) => (
                            <div key={photo.id}>
                                <img src={photo.thumbnailUrl} alt={photo.title} />
                                <p>{photo.title}</p>
                            </div>
                        ))}
                    </React.Fragment>
                ))}
            </div>
            {isFetchingNextPage && <p>Loading more...</p>}
            <div ref={observerRef} style={{ height: '1px' }}></div>
        </div>
    );
};
export default InfiniteScroll;
- 장점- 내장된 페이지네이션 처리로 무한 스크롤 구현이 간단.
- getNextPageParam으로 다음 페이지 매개변수 설정.
- isFetchingNextPage상태로 로딩 상태 관리 가능.
 
- 단점
import React, { useEffect, useRef, useState } from 'react';
import { useQuery } from 'react-query';
interface Photo {
    id: number;
    title: string;
    url: string;
    thumbnailUrl: string;
}
const fetchPhotos = async (page: number): Promise<Photo[]> => {
    const res = await fetch(`https://jsonplaceholder.typicode.com/photos?_page=${page}&_limit=10`);
    return res.json();
};
const InfiniteScroll: React.FC = () => {
    const [page, setPage] = useState<number>(1);
    const [photos, setPhotos] = useState<Photo[]>([]);
    const observerRef = useRef<HTMLDivElement | null>(null);
    const { data, error, isLoading } = useQuery(['photos', page], () => fetchPhotos(page), { keepPreviousData: true });
    useEffect(() => {
        if (data) {
            setPhotos((prevPhotos) => [...prevPhotos, ...data]);
        }
    }, [data]);
    useEffect(() => {
        const observer = new IntersectionObserver((entries) => {
            if (entries[0].isIntersecting) {
                setPage((prevPage) => prevPage + 1);
            }
        });
        if (observerRef.current) {
            observer.observe(observerRef.current);
        }
        return () => {
            if (observerRef.current) {
                observer.unobserve(observerRef.current);
            }
        };
    }, []);
    if (error) return <div>Failed to load</div>;
    return (
        <div>
            <h1>Infinite Scroll</h1>
            <div>
                {photos.map((photo) => (
                    <div key={photo.id}>
                        <img src={photo.thumbnailUrl} alt={photo.title} />
                        <p>{photo.title}</p>
                    </div>
                ))}
            </div>
            {isLoading && <p>Loading...</p>}
            <div ref={observerRef} style={{ height: '1px' }}></div>
        </div>
    );
};
export default InfiniteScroll;
- 장점- 간단한 페이지네이션 로직 구현 가능.
- 상태를 useState로 관리하여 코드 가독성 높음.
 
- 단점- useInfiniteQuery에 비해 많은 코드 작성 필요.
- 무한 스크롤 로직 수동 처리.
 
import React, { useEffect, useRef, useState } from 'react';
import useSWR from 'swr';
interface Photo {
    id: number;
    title: string;
    url: string;
    thumbnailUrl: string;
}
const fetcher = (url: string) => fetch(url).then((res) => res.json());
const InfiniteScroll: React.FC = () => {
    const [page, setPage] = useState<number>(1);
    const [photos, setPhotos] = useState<Photo[]>([]);
    const observerRef = useRef<HTMLDivElement | null>(null);
    const { data, error } = useSWR<Photo[]>(`https://jsonplaceholder.typicode.com/photos?_page=${page}&_limit=10`, fetcher);
    useEffect(() => {
        if (data) {
            setPhotos((prevPhotos) => [...prevPhotos, ...data]);
        }
    }, [data]);
    useEffect(() => {
        const observer = new IntersectionObserver((entries) => {
            if (entries[0].isIntersecting) {
                setPage((prevPage) => prevPage + 1);
            }
        });
        if (observerRef.current) {
            observer.observe(observerRef.current);
        }
        return () => {
            if (observerRef.current) {
                observer.unobserve(observerRef.current);
            }
        };
    }, []);
    if (error) return <div>Failed to load</div>;
    return (
        <div>
            <h1>Infinite Scroll</h1>
            <div>
                {photos.map((photo) => (
                    <div key={photo.id}>
                        <img src={photo.thumbnailUrl} alt={photo.title} />
                        <p>{photo.title}</p>
                    </div>
                ))}
            </div>
            <div ref={observerRef} style={{ height: '1px' }}></div>
        </div>
    );
};
export default InfiniteScroll;
- 장점- 자동 데이터 캐싱 및 가져오기 관리.
- 간단한 설정으로 구현 가능.
 
- 단점- 추가 로직 필요.
- react-query보다 명확한 내장 옵션 부족