Infinite Scroll

김준엽·2022년 5월 11일
1

React

목록 보기
7/11

Intersection Observer API을 이용해서 Infinit Scroll을 구현해 보겠습니다. Intersection Observer API 동작 방법은 target element가 root element에 보이면 설정한 콜백함수가 실행됩니다.

useInfiniteScroll.tsx

import { useEffect, useRef } from 'react'

interface IProps {
  rootMargin?: string
  threshold?: number
  disable: boolean
  hasNextPage: boolean
  fetchCallback: (page: number) => Promise<void>
}

export default function useInfiniteScroll({
  rootMargin = '0px',
  threshold = 0,
  disable,
  hasNextPage,
  fetchCallback,
}: IProps) {
  const page = useRef(1)
  const rootRef = useRef<HTMLDivElement>(null)
  const targetRef = useRef<HTMLLIElement>(null)

  useEffect(() => {
    if (!targetRef.current || disable || !hasNextPage) return undefined

    const handleObserver = (entries: IntersectionObserverEntry[]) => {
      const target = entries[0]
      if (target.isIntersecting) {
        page.current += 1
        fetchCallback(page.current)
      }
    }

    const option = {
      root: rootRef.current,
      rootMargin,
      threshold,
    }
    const observer = new IntersectionObserver(handleObserver, option)
    observer.observe(targetRef.current)

    return () => {
      observer && observer.disconnect()
      page.current = 1
    }
  }, [disable, fetchCallback, hasNextPage, rootMargin, threshold])

  return { targetRef, rootRef }
}
  • page는 useRef를 이용했습니다. 이유는 useState를 사용하면 useEffect에 의존성 배열에 포함하고 fetchCallback을 외부로 빼서 실행해야 되기 때문입니다.
  • useEffect 의존성 배열에 있는 상태가 변경되면 clean-up함수가 실행되서 관찰을 멈춥니다.
  • IntersectionObserver option
    • root
      • default : null, 브라우저 viewport
      • 교차 영역 기준이 될 root Element
    • rootMargin
      • default : 0px
      • root Element margin입니다. 이 값에 따라 교차영역이 확장 또는 축소됩니다.
    • threshold
      • default : 0
      • 0.0 ~ 1.0에서 설정합니다. target Element 교차 영역 비율을 의미합니다. 예를 들어 0.5이면 target이 50%정도 보이면 observer를 실행합니다.

Component.tsx

import { useCallback, useEffect, useState } from 'react'
import { useRecoilState, useRecoilValue } from 'recoil'
import { searchInput, isInitSearch } from 'recoil/search'
import { ISearchItem } from 'types/search'
import { getSearchResApi } from 'services/search'
import useInfiniteScroll from 'hooks/useInfiniteScroll'

import SearchItem from './SearchItem'
import styles from './SearchResult.module.scss'

const fetchSearchData = async (s: string, page: number) => {
  const { data } = await getSearchResApi({ s, page })
  const { Search: searchItems, totalResults } = data
  const endPage = Math.ceil(Number(totalResults) / 10)

  return { searchItems, endPage }
}

export default function SearchResult() {
  const [isInit, setIsInit] = useRecoilState(isInitSearch)
  const searchInputVal = useRecoilValue(searchInput)
  const [items, setItems] = useState<ISearchItem[]>([])
  const [hasNextPage, setHasNextPage] = useState(false)

  const fetchCallback = useCallback(
    async (page: number) => {
      if (searchInputVal === '') return

      const { searchItems, endPage } = await fetchSearchData(searchInputVal, page)
      setItems((prev) => [...prev, ...searchItems])
      setHasNextPage(page < endPage)
    },
    [searchInputVal]
  )

  const { targetRef } = useInfiniteScroll({
    disable: items.length === 0,
    hasNextPage,
    fetchCallback,
  })

  useEffect(() => {
    if (!isInit) return
    setHasNextPage(false)
    setItems([])
    setIsInit(false)
  }, [isInit, setIsInit])

  useEffect(() => {
    fetchCallback(1)
  }, [fetchCallback])

  return (
    <article className={styles.wrapper}>
      {items.length === 0 ? (
        <div>검색 결과가 없습니다.</div>
      ) : (
        <ul>
          {items.map((item: ISearchItem, index) => {
            const key = `searchItem-${index}`
            return <SearchItem key={key} item={item} />
          })}
          <li className={styles.endPoint} ref={targetRef} />
        </ul>
      )}
    </article>
  )
}
  • Infinite Scroll hooks를 사용한 컴포넌트입니다.
  • 비동기 fetch가 실행되는 fetchCallback에서 화면에 출력할 items와 다음 페이지가 있는 여부를 나타내는 hasNextPage에 값을 할당합니다.

참고

infinite-scroll
intersection-observer 1
intersection-observer 2

profile
프론트엔드 개발자

0개의 댓글