- Published on
React Infinite Scroll 구현하기
- Authors

- Name
- Hyo814
무한 스크롤은 많은 웹 애플리케이션에서 데이터 로딩 경험을 개선하기 위해 자주 사용되는 패턴입니다. React에서 무한 스크롤을 구현하는 방법은 여러 가지가 있는데, 이번 글에서는 @tanstack/react-query의 useInfiniteQuery와 useQuery, 그리고 swr의 useSWR을 사용하여 무한 스크롤을 구현하는 방법을 비교해보려고 합니다.
1. @tanstack/react-query의 useInfiniteQuery 사용 예제
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
장점
- 페이지네이션 처리 내장:
useInfiniteQuery는 페이지네이션이 내장되어 있어 다음 페이지 데이터를 손쉽게 받아옵니다. - 간단한 설정:
getNextPageParam함수로 다음 페이지 매개변수가 자동 설정되어, 무한 스크롤 로직이 간결해집니다. - 로딩 상태 관리:
isFetchingNextPage같은 상태로 로딩 표시를 쉽게 다룹니다.
단점
- 설정 복잡성:
react-query의 개념을 충분히 이해해야 설정을 제대로 할 수 있습니다.
2. @tanstack/react-query의 useQuery 사용 예제
import React, { useEffect, useRef, useState } from 'react'
import { useQuery } from '@tanstack/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보다 코드량이 늘어납니다.
3. SWR의 useSWR 사용 예제
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
장점
- 간단한 설정:
SWR은 데이터 패칭과 캐싱을 자동으로 처리해 설정이 간결하고 쓰기 편합니다. - 자동화된 데이터 관리: 데이터를 효율적으로 받아오고 캐싱합니다.
단점
- 무한 스크롤 추가 로직 필요: 무한 스크롤은 별도 로직을 직접 붙여야 합니다.
- 내장 옵션 부족:
react-query처럼 다음 페이지 데이터를 명확하게 다루는 내장 옵션이 부족합니다.