- Published on
React Infinite Scroll 구현하기
- Authors
- Name
- Hyo814
무한 스크롤은 많은 웹 애플리케이션에서 데이터 로딩 경험을 개선하기 위해 자주 사용되는 패턴입니다. React에서 무한 스크롤을 구현하는 방법은 여러 가지가 있는데, 이번 글에서는 @tanstack/react-query
의 useInfiniteQuery
와 useQuery
, 그리고 swr
의 useSWR
을 사용하여 무한 스크롤을 구현하는 방법을 비교해보려고 합니다.
useInfiniteQuery
사용 예제
1. @tanstack/react-query의 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
의 개념을 잘 이해해야만 설정을 제대로 할 수 있습니다.
useQuery
사용 예제
2. @tanstack/react-query의 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
에 비해 더 많은 코드 작성이 필요합니다.
useSWR
사용 예제
3. SWR의 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
처럼 다음 페이지 데이터를 명확하게 처리하기 위한 내장 옵션이 부족합니다.