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

송수빈송수빈
3 min read

이번 프로젝트에서 무한스크롤 구현을 맡았다 👏🏻

현재는 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: 로딩 중일 때 중복 호출 방지하기

  • 그리고 로딩할 게시글이 없으면 직관적으로 더 이상 불러올 게시글이 없습니다.를 넣어줘서 “끝”을 명확히 알려주었다 ✨

게시글이 많지 않아서 무한스크롤이 잘 되는지 눈으로는 확인이 어려워서 백엔드한테 데이터 넣어달라고 부탁함 ㅎㅡㅎ.. 감사함둥

스크롤 하면서 네트워크 탭에 계속 찍히는 걸 확인할 수 있다 👍🏻

0
Subscribe to my newsletter

Read articles from 송수빈 directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

송수빈
송수빈