[React] Intersection Observer 사용해서 무한스크롤 구현하기 🍞


이번 프로젝트에서 무한스크롤 구현을 맡았다 👏🏻
현재는 6개 이상의 게시물을 만들면 가장 오래된 게시글이 안 보인다
이전 프로젝트에서는 무한스크롤을 API 없이 순수 구현했는데 이번에는 Intersection Observer API를 사용해서 구현하려고 한다.
🧐 일단 왜 무한스크롤
을 선택했나에 대해서 설명하자면
사용자의 클릭을 최소화 해서 데이터를 편하게 보여주고 싶은 마음
요즘 페이지네이션 보다 무한스크롤을 많이 사용하는 추세라고 생각
해서 선택하게 되었다.
✏️ 무한스크롤 구현하기
게시물을 6개씩 불러온다
스크롤이 바닥에 닿을 때 다음 게시물들을 요청한다
데이터 요청 중에는 로딩 스피너를 보여준다
원래는 뒤로가기 시 기존 게시물 목록 위치를 유지하려 했지만,, ! 우리는 상세페이지 하단에 현재 게시물 제외한 나머지 게시물들을 계속 보여줄 것이기 때문에 별도 상태 저장 없이 다시 목록을 불러와도 무방하다고 생각했다!
📜 그래서 Intersection Observer가 뭐야?
쉽게 말해서 화면에 들어왔는지 자동으로 알려주는 도구
스크롤하다가 어떤 요소가 눈에 들어오면 → 자동으로 감지해서 알려준다
스크롤이나 요소의 가시 상태(화면에 보이는지 여부)를 관찰할 수 있게 해주는 브라우저 API이다.
👉🏻 그럼 이제 구현해보자 !
- 일단 무한스크롤을 구인 & 구직 & 자유게시판에서 공통적으로 사용할 기능이라 따로 컴포넌트로 빼줬다.
import { useCallback, useEffect, useRef } from 'react'
const InfiniteScrollList = ({ children, onIntersect, disabled }) => {
const loaderRef = useRef() //DOM 참조 생성
const handleIntersect = useCallback(
(entries) => {
const [entry] = entries
if (entry.isIntersecting) {
onIntersect() // 뷰포트에 들어오면 onIntersect 호출
}
},
[onIntersect], // onIntersect가 변경되면 콜백도 재생성됨
)
useEffect(() => {
if (disabled) return
const observer = new IntersectionObserver(handleIntersect, {
rootMargin: '100px',
threshold: 0.1, // 요소의 10%가 보여지면 콜백 트리거
})
const loader = loaderRef.current
if (loader) observer.observe(loader) // 대상 요소 감시 시작
return () => {
if (loader) observer.unobserve(loader)
}
}, [handleIntersect, disabled]) // disabled나 콜백 변경 시 useEffect 재실행
return (
<>
{children}
<div ref={loaderRef} className='w-full h-10' />
</>
)
}
export default InfiniteScrollList
API 요청하는 부분
export const getAllPosts = async ({ last = null, size = 6 } = {}) => {
try {
const response = await publicApi.get('/?????', {
params: last ? { last, size } : { size },
})
return response.data.data
} catch (error) {
console.error('게시글 조회 실패', error)
throw error
}
}
size
를 삽입해주었다.(6개씩 불러오기로 사전에 백엔드랑 회의 완료✅)
hook(useCommunityPost.js)
- 기존 코드에서
hasMore
를 추가해주어서 더 불러올 게시물이 있는지에 대한 여부를 확인했다.
const [hasMore, setHasMore] = useState(true) // 더 불러올 게시글이 있는지 여부 삽입
const loadMore = useCallback(async () => {
if (loading || !hasMore) return
const lastId = posts.length > 0 ? posts[posts.length - 1].boardId : null
setLoading(true)
try {
const { boards } = await getAllPosts({ last: lastId, size: 6 })
if (boards.length === 0) {
setHasMore(false) // 새 게시글이 없으면 hasMore를 false로 바꿔 무한스크롤 끝 알림 기능 구현
} else {
setPosts((prev) => {
const ids = new Set(prev.map((p) => p.boardId))
const newBoards = boards.filter((b) => !ids.has(b.boardId))
return [...prev, ...newBoards]
})
}
} catch (error) {
console.error('게시글 불러오기 실패:', error)
} finally {
setLoading(false)
}
}, [loading, hasMore, posts])
useEffect(() => {
loadMore()
}, [])
return {
posts,
loading,
hasMore,
loadMore,
}
}
export default useCommunityPosts
CommunityMainPage.jsx
const { posts, loading, hasMore, loadMore } = useCommunityPosts()
<InfiniteScrollList
onIntersect={() => {
if (!hasSearched) {
const lastId = posts.length > 0 ? posts[posts.length - 1].boardId : null
loadMore(lastId)
}
}}
disabled={!hasMore || loading || hasSearched}
>
.....
</InfiniteScrollList>
{!hasMore && !loading && !hasSearched && (
<div className='text-center text-gray-500 my-6'>
더 이상 불러올 게시글이 없습니다.
</div>
)}
</>
이런식으로 카드를 랜더링 하는 부분에 InfiniteScrollList
를 적용해주었다.
!hasSearched
: 검색 중이 아닐 때만 무한 스크롤 작동시키기!hasMore
: 더 불러올 게시글이 없으면 감시 중지하기loading
: 로딩 중일 때 중복 호출 방지하기그리고 로딩할 게시글이 없으면 직관적으로
더 이상 불러올 게시글이 없습니다.
를 넣어줘서 “끝”을 명확히 알려주었다 ✨
게시글이 많지 않아서 무한스크롤이 잘 되는지 눈으로는 확인이 어려워서 백엔드한테 데이터 넣어달라고 부탁함 ㅎㅡㅎ.. 감사함둥
스크롤 하면서 네트워크 탭에 계속 찍히는 걸 확인할 수 있다 👍🏻
Subscribe to my newsletter
Read articles from 송수빈 directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
