원글 페이지 : 바로가기

메인 화면에 포스트 목록을 무한스크롤로 구현하기로 했다. 기존 프로젝트의 경우, 백엔드에서 제공하는 데이터에 다음 페이지에 관한 정보가 있었기에 무난히 구현되었지만, 이번엔 supabase에서 그대로 받아오기 때문에 다음 페이지에 대한 정보가 없다. 그래서 페이지네이션에서 사용했던 형태로 시도해 보았더니.. 역시 마음처럼 작동하지 않았다. 1. 무한스크롤이 바로 작동 말 그대로 스크롤을 내리지 않아도 무한스크롤이 자동으로 작동하는 현상 2. 무한스크롤이 작동하지 않음 그냥 무한 스크롤이 작동하지 않는 현상 위와 같은 무한 스크롤 구현 시 가장 자주 보이는 문제가 있었고, 그냥 페이지 정보를 받아오는게 편할까 싶어 찾아보니 https://velog.io/@hw1635/TIL-%EB%AC%B4%ED%95%9C%EC%8A%A4%ED%81%AC%EB%A1%A4-%EC%A0%95%EB%B3%B5%EA%B8%B0-useInfinitieQuery-with-supabase [TIL] 무한스크롤 정복기 (useInfinitieQuery with supabase) 기존에 장소 검색 페이지에서 무한 스크롤을 구현 했었으나,react-query의 useInfiniteQuery를 사용하지 않고, 조잡한(?) 방식으로 처리하고 있었다.그러다 보니 코드 가독성도 매우 떨어지고, 중간 중간 velog.io 요런 글이나 https://velog.io/@o1011/Next.js14-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-supabase-%EB%A1%9C-%EB%AC%B4%ED%95%9C%EC%8A%A4%ED%81%AC%EB%A1%A4-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0#supabase-%EB%A1%9C-%ED%8E%98%EC%9D%B4%EC%A7%80-%EC%A0%95%EB%B3%B4-%EB%B0%9B%EC%95%84%EC%98%A4%EA%B8%B0 [Next.js14 프로젝트] useInfinitieQuery, supabase 로 무한스크롤 구현하기 supabase 로 무한스크롤 구현하기 보통 응답을 받을때 페이지 정보를 담은 meta 정보를 같이 전달 받는데, supabase 로 서버를 구현하다보니 getNextPageParam getNextPageParam 함수는 useInfiniteQuery 에서 다음 velog.io 요런 글이 있었고, 둘 다 explain 함수를 사용해야 했다. 그러나 함수가 더 길어지는게 보기 싫었던 나는 explain 없이 구현하고자 했고, 생각보다 큰 문제 없이 구현에 성공했다. 이 영광을 tanstack-query 공식 문서에게 바칩니다. useInfiniteQuery? 내 코드와 함께 간단하게 설명해보겠다. useInfiniteQuery는 무한 스크롤을 처리하기 위한 React Query 의 훅이다. 데이터 페칭, 캐싱, 무한 스크롤 등의 기능을 제공하여 페이지네이션을 용이하게 해준다. const pageSize = 8
const {
data: allPosts,
hasNextPage,
isFetching,
fetchNextPage,
} = useInfiniteQuery({
queryKey: [‘allPosts’],
queryFn: ({ pageParam = 1 }) => getAllPosts(pageParam, pageSize),
getNextPageParam: (lastPage, __, lastPageParam) => {
if (lastPage.data.length < pageSize) return undefined
return lastPageParam + 1
},
initialPageParam: 1, // 초기 페이지 설정
})
const { observerEl, isLoading } = useInfiniteScroll({
callbackFn: () => {
if (!isLoading) {
fetchNextPage()
}
},
hasNextPage,
})
const flattenedData = allPosts?.pages.flatMap((page) => page.data) ?? [] queryKey: 쿼리를 식별하는 고유한 키. 난 [‘allPosts’]로 지었다. queryFn: 데이터 fetching 함수. pageParam, pageSize를 인자로 받는 getAllPosts() 함수를 실행, 해당 페이지의 데이터를 요청. getNextPageParam: 다음 페이지를 요청할 때 사용하는 페이지 파라미터를 결정하는 함수. 이 함수는 현재 페이지(lastPage)의 데이터와 전체 데이터를(__)를 기준으로 다음 페이지 번호를 반환. initialPageParam: 초기 페이지 번호 설정. Intersection Observer Intersection Observer API는 기본적으로 브라우저 Viewport와 Target으로 설정한 요소의 교차점을 관찰하여 그 Target이 Viewport에 포함되는지 구별하는 기능을 제공한다. 출처: https://tech.kakaoenterprise.com/149 [카카오엔터프라이즈 기술블로그 Tech&(테크앤):티스토리] Intersection Observer를 통해 스크롤이 target을 넘기면 무한스크롤이 작동되도록 했다. useInfiniteScroll.ts import { useState, useEffect, useRef, useCallback } from ‘react’
type InfiniteScrollType = {
callbackFn: () => void // 데이터를 로드하는 콜백 함수
hasNextPage: boolean // 다음 페이지가 있는지 여부를 나타내는 플래그
}
export const useInfiniteScroll = ({ callbackFn, hasNextPage }: InfiniteScrollType) => {
const [isLoading, setIsLoading] = useState
const observerEl = useRef
// Intersection Observer의 콜백 함수 정의
const handleObserver = useCallback(
(entries: IntersectionObserverEntry[]) => {
const target = entries[0] // 관찰된 엔트리 중 첫 번째를 선택
// 타겟이 뷰포트와 교차하고 로딩 중이지 않으며 다음 페이지가 있는 경우
if (target.isIntersecting && !isLoading && hasNextPage) {
setIsLoading(true)
callbackFn() // 데이터를 로드하는 콜백 함수 호출
setIsLoading(false)
}
},
[callbackFn, isLoading, hasNextPage] // 콜백 함수와 상태가 변경될 때마다 콜백 함수가 재생성
)
useEffect(() => {
const observer = new IntersectionObserver(handleObserver, { threshold: 0 }) // Intersection Observer 인스턴스 생성
const currentEl = observerEl.current // 현재 관찰 중인 엘리먼트 참조 가져오기
if (currentEl) {
observer.observe(currentEl) // 엘리먼트를 관찰 시작
}
return () => {
if (currentEl) {
observer.unobserve(currentEl) // 컴포넌트가 언마운트되거나 업데이트될 때 엘리먼트의 관찰을 해제
}
}
}, [handleObserver]) // handleObserver가 변경될 때마다 useEffect가 실행됨
return { observerEl, isLoading } // 관찰할 엘리먼트 참조와 로딩 상태를 반환
} 로직은 정말 간단하다! inintialPageParama(1페이지)부터 8개씩 받아오는데, 다음 페이지가 있다면(현재 페이지의 데이터의 length가 pageSize보다 작지 않다면) pageParam을 +1 더해준다. 그 후 intersectionObserve를 통해 관찰중인 요소가 교차하면(스크롤을 내리면) 증가한 pageParam부터 pageSize만큼 데이터를 또 불러온다! 그러나 문제가 아직 남아있었다. 만약 전체 데이터가 16개인데, pageSize가 8이라면? 원래라면 첫 번째는 당연히 다음 페이지가 있지만 두 번째 호출 이후엔 함수가 호출되지 않아야 한다. 그러나 위의 함수대로 한다면 export const getAllPosts = async (pageParam: number, pageSize: number) => {
console.log(‘호출’, pageParam) // ‘호출’ | 3, ‘호출’ | 2, ‘호출’ | 1
const supabase = supabaseServer()
const { data, error } = await supabase
.from(‘posts’)
.select(‘*,users!inner(nickname,penname,profile_url)’)
.order(‘created_at’, { ascending: false })
.range((pageParam – 1) * pageSize, pageParam * pageSize – 1)
if (error) {
throw new Error(error.message)
}
return { data }
} 위와 같이 세 번째 호출이 생길것이다. 이래선 안된다! 이런 현상을 막기 위해 전체 데이터의 개수와 지금까지 불러온 데이터의 개수를 비교해야 한다. 마침 supabase에선 전체 데이터의 개수를 불러오는 아주 좋은 친구가 있다. // 생성일 기준으로 모든 포스트 fetch
export const getAllPosts = async (pageParam: number, pageSize: number) => {
console.log(‘발동’, pageParam)
const supabase = supabaseServer()
const { data, count, error } = await supabase
.from(‘posts’)
.select(‘*,users!inner(nickname,penname,profile_url)’, { count: ‘exact’ })
.order(‘created_at’, { ascending: false })
.range((pageParam – 1) * pageSize, pageParam * pageSize – 1)
if (error) {
throw new Error(error.message)
}
return { data, count }
} select에 count를 통해 전체 데이터를 받아올 수 있게 되었다. 이제 이 count를 가지고 query를 수정하면 된다! const {
data: allPosts,
hasNextPage,
isFetching,
fetchNextPage,
} = useInfiniteQuery({
queryKey: [‘allPosts’],
queryFn: ({ pageParam = 1 }) => getAllPosts(pageParam, pageSize),
getNextPageParam: (lastPage, __, lastPageParam) => {
if (lastPage.data.length < pageSize) return undefined
const totalPages = Math.ceil(lastPage.count! / pageSize) // 전체 페이지 수 계산
if (lastPageParam >= totalPages) {
return undefined // 마지막 페이지를 넘어서지 않도록 설정
}
return lastPageParam + 1
},
initialPageParam: 1, // 초기 페이지 설정
})
export const getAllPosts = async (pageParam: number, pageSize: number) => {
console.log(‘호출’, pageParam) // ‘호출’ | 2, ‘호출’ | 1
— 중략 —
return { data, count }
} 이제 무한스크롤 끝!