프론트엔드 개발자의 기록 공간

[React 무한 스크롤 구현하기] (feat. typeScript, useHook) 본문

개발지식

[React 무한 스크롤 구현하기] (feat. typeScript, useHook)

[리우] 2022. 10. 22. 19:04

무한 스크롤은 많은 데이터를 세분화하여 필요시에 데이터를 요청해서 받아오면서 성능을 극대화한다. react+typeScript에서 무한 스크롤을 구현하는 방법을 useHook으로 관리하여 사용하는 법을 소개할 것이다.

 

Intersection Observer API

Intersection Observer API는 대상 요소가 상위 요소 또는 최상위 문서의 뷰포트 와 교차하는 변경 사항을 비동기식으로 관찰하는 방법을 제공합니다. 따라서 Scroll 이벤트를 걸어서 스크롤 할 때마다 함수가 실행되는 스크롤 이벤트보다 훨씬 성능이 좋다.

 

파일 구성

useObserver (무한스크롤 useHook)
Item (무한스크롤하는 개별 요소)
Main (Item List를 생성하는 상위 컴포넌트)

 

useObserver.ts

import { useEffect, useState } from 'react'

const MaxPAge = 10 // 페이지 최대 사이즈

const useObserver = () => {
  const [page, setPage] = useState(1) // pagination을 위한 변수
  const [isFetching, setIsFetching] = useState(true) // Loading처리를 위한 변수
  const [lastIntersecting, setLastIntersecting] =
    useState<HTMLDivElement | null>(null) // 구독할 타켓 정보

  //observer 콜백함수
  const onIntersect: IntersectionObserverCallback = (entries, observer) => {
    entries.forEach((entry) => {
      // 뷰 포트에 마지막 요소가 들어올 때,
      if (entry.isIntersecting) {
        // 페이지 최대가 넘어가지 않을 때
        if (page < MaxPAge) {
          // page값에 1을 더하여 새 fetch 요청을 보내게됨
          setPage((prev) => prev + 1)
          setIsFetching(true)
        }
        // 기존 타겟을 unobserve한다.
        observer.unobserve(entry.target)
      }
    })
  }

  useEffect(() => {
    if (!lastIntersecting) return
    //observer 인스턴스를 생성한 후 구독
    const observer = new IntersectionObserver(onIntersect, { threshold: 0.5 })
    //observer 생성 시 observe할 target 요소는 배열의 마지막 타켓으로 지정
    observer.observe(lastIntersecting)

    return () => observer && observer.disconnect()
  }, [lastIntersecting])

  // 사용할 hook state값 내보내기
  return { page, setPage, isFetching, setIsFetching, setLastIntersecting }
}

export default useObserver

1. 필요한 state 값 정의하기

  • 무한 스크롤 발생 시 api 요청에 넣을 page state를 정의한다.
  • 로딩 처리 UI가 필요하다면 isFetching state를 정의한다.
  • 무한 스크롤을 발생시키는 조건인 타켓의 정보를 가진 lastIntersection state를 정의한다.

2. 옵저버 타켓(lastIntersection)을 의존성을 넣은 useEffect hook을 만들어준다.

  • 생성자를 호출하고 임계값이 한 방향 또는 다른 방향으로 교차할 때마다 실행할 콜백 함수를 전달하여 교차 관찰자를 만듭니다. 두 번째 인자로 부가적인 옵션을 지정할 수 있습니다.
  • observer.onserve(target)을 통해 관찰자를 생성한 후에는 대상 요소를 지정한다.
  • 마지막으로 return을 통해 후속처리 작업을 진행해 준다. (등록한 관찰자 제거)

3. 콜백 함수를 정의한다.

  • onIntersect 함수는 관찰자 대상이 지정한 임계값 (threshold)을 충족할 때 호출된다. 콜백은 IntersectionObserverEntry객체 목록과 관찰자를 수신합니다.
  • 뷰 포트에 마지막 요소가 들어오면 처리할 작업을 지정한다. (여기에서는 최대 10번만 무한 스크롤 발생을 위해 10보다 작을 경우에만 페이지 요소를 증가시켜 api에 해당 page 요소를 함께 보내도록 작업)
  • 마지막으로 앞에 지정했던 타켓을 관찰자 대상에서 제거한다.

 

Main

const Main = () => {
  const [list, setList] = useState<TItem[]>([])
  const { page, setPage, isFetching, setIsFetching, setLastIntersecting } =
    useObserver() // 정의한 observer에서 useHook가져오기

  useEffect(() => {
    ;(async () => {
      const data = await getCharacterAPI(page)
      data && setCharacterList([...list, ...data])
    })()
  }, [page])

  return (
    <>
      {list?.map((data, idx) => (
        <div ref={setLastIntersecting}> // 구독할 대상
          <Item data={data}/>
        </div>
      ))}
      // 로딩 처리
      {isFetching && (
        <div style={{ textAlign: 'center' }}>
          <Spinner />
        </div>
      )}
    </>
  )
}
1. List를 순회하면서 하위 컴포넌트를 생성해 주고 ref로 구독할 element를 정의한다.
2. page가 변경될 때 api 요청을 위해 useEffect hook을 이용하여 page state를 의존성으로 넣어준다.
  • 기존 데이터에 새로 받아온 데이터를 누적시켜서 정의해 준다.

 

※ 참고

무한 스크롤은 서버의 데이터가 존재하지 않으면 무한 스크롤을 더 이상 발생시키면 안 된다.
따라서 서버에서 마지막 데이터를 알려주는 정보가 있다면 반환된 api 정보를 가지고 마지막 데이터라면 어떠한 상태를 두고 false 값으로 지정하여 더 이상 무한 스크롤을 발생시키지 않도록 한다.

  const [lastPage, setLastPage] = useState(true)
  ...
  ...

  
 if (entry.isIntersecting) {
    // 페이지 최대가 넘어가지 않을 때
    if (lastPage) {
      // page값에 1을 더하여 새 fetch 요청을 보내게됨
      setPage((prev) => prev + 1)
      setIsFetching(true)
    }
    // 기존 타겟을 unobserve한다.
    observer.unobserve(entry.target)
 }
 
return { ..., setLastPage }
Main

useEffect(() => {
;(async () => {
  const data = await getCharacterAPI(page)
  if(data.last) setLastPage(false) // api에서 마지막 데이터를 반환 받은 경우
  data && setCharacterList([...list, ...data])
})()
}, [page])

마지막 페이지 정보임을 나타내는 state를 하나 더 정의하여 옵저버를 통해 page state 바꾸기 전에 체크한다.

그리고 setLastPage를 함께 상태를 내보내서 Main에서 api를 통해 반환된 정보를 통해 setLastPage(false) 로 상태를 업데이트하여 사용하면 된다.

728x90
Comments