개발

react 무한스크롤 intersection observer 다루기 -1

배우겠습니다 2024. 6. 15. 16:39

개요

해당 카카오테크 블로그를 통해 무한스크롤을 많이 구현하는 것 같다.

따라서 이 글은 저 블로그 코드가 어떻게 동작하는지에 대해 설명하고, 다음 글에선 난 어떤 방식으로 Intersection observer을 다룰건지에 대해 포스팅한다.

tanstack-query의 useInfiniteQuery, intersection observer, 저 블로그글에 대해 접해본 사람이 본다면 좋을 것 같다.

 

IntersectionObserser API

let options = {
  root: document.querySelector("#scrollArea"),
  rootMargin: "0px",
  threshold: 1.0,
};

let observer = new IntersectionObserver(callback, options);

관찰자 생성과 관리를 위한 메서드(모든 대상 주시 해제, 대상 요소 주시, 모든 주시 대상에 대한 entries 반환, 특정 대상 주시 해제)를 제공한다.

교차한다면 callback을 실행한다.

 

IntersectionObserverEntry

Entry는 IntersectionObserver가 요소에 대해 제공하는 정보이다.

attribute 설명 타입
boundingClientRect element의 위치와 크기, 경계 상자 정보 DOMRect
intersectionRatio root과 element의 교차 비율 number(0.0~1.0)
intersectionRect root과 element가 실제로 교차하는 영역 경계 상자 정보 DOMRect
isIntersecting 교차중인가? boolean
rootBounds root(대부분 뷰포트)의 경계 상자 정보 DOMRect
time 교차가 발생한 시간 number(ms)
target 관찰중인 element Element

대부분 entry는 배열로 주어지고 사용하는데, 왜냐하면 하나의 IntersectionObserver은 여러 요소를 관찰할 수 있고(entry마다 각각의 Observer을 만들 필요 없음) 이에 따라 배치처리도 지원하기 때문이다.

 

무한스크롤 구현

function UsersPage() {
 const { data, hasNextPage, isFetching, fetchNextPage } = useFetchUsers({
   size: PAGE_SIZE,
 })
 const users = useMemo(
   () => (data ? data.pages.flatMap(({ data }) => data.contents) : []),
   [data]
 )
 
 const ref = useIntersect(async (entry, observer) => {
   observer.unobserve(entry.target)
   if (hasNextPage && !isFetching) {
     fetchNextPage()
   }
 })
 
 return (
   <Container>
     {users.map((user) => (
       <Card key={user.id} name={user.name} />
     ))}
     {isFetching && <Loading />}
     <Target ref={ref} />
   </Container>
 )
}

 

1. useFetchUsers

useInfiniteQuery 로직을 커스텀훅으로 분리한 것이다. 생략한다. (추후 요청이 있다면 useInfiniteQuery에 대한 포스트를 쓰겠다.)

fetchNextPage()로 다음 데이터를 페칭한다.

2. users

무한 스크롤에 표시할 데이터들이다. 페칭한 데이터들이 쌓인다.

3. 로직

useIntersect custom hook(추후 설명)에 함수를 넣는다. 이 함수는 어떤 "트리거"에 따라 동작할 함수이다.

해당 커스텀훅은 ref를 리턴한다. Target에 ref를 적용해 Target이 보인다는 "트리거"가 있다면 useIntersect에 넣어준 함수가 동작한다.

해당 함수를 살펴보자.

 const ref = useIntersect(async (entry, observer) => {
   observer.unobserve(entry.target)
   if (hasNextPage && !isFetching) {
     fetchNextPage()
   }
 })

아직 useIntersect의 내부는 모르지만, param으로 들어간 함수가 무엇을 위해 존재하는지, 어떤 기능을 하는지는 추측할 수 있다.

1. 관찰자가 요소의 관찰을 해제한다.

2. 다음페이지가 존재하고, 데이터를 페칭중이 아니라면 다음 페이지 데이터를 페칭한다.

왜 관찰을 해제하는 것일까?

만약 hasNextPage && isFetching이 true라면 다음 페이지 데이터를 페칭하는데, 이때 관찰을 해제해 불필요한 중복 페칭을 방지하는 것이다.

 

useIntersect

// [코드 11] IntersectionObserver custom hook
type IntersectHandler = (
 entry: IntersectionObserverEntry,
 observer: IntersectionObserver
) => void
 
const useIntersect = (
 onIntersect: IntersectHandler,
 options?: IntersectionObserverInit
) => {
 const ref = useRef<HTMLDivElement>(null)
 const callback = useCallback(
   (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => {
     entries.forEach((entry) => {
       if (entry.isIntersecting) onIntersect(entry, observer)
     })
   },
   [onIntersect]
 )
 
 useEffect(() => {
   if (!ref.current) return
   const observer = new IntersectionObserver(callback, options)
   observer.observe(ref.current)
   return () => observer.disconnect()
 }, [ref, options, callback])
 
 return ref
}

1. ref

target에 적용할 ref

2. callback

교차하면 실행될 callback

만약 어떤 요소가 교차한다면, onIntersect(다음 페이지 데이터 페칭)를 실행한다.

랜더링시 해당 callback이 재생성됨을 막기 위해 useCallback을 씌운다.

3. useEffect

ref.current에 요소가 할당되지 않았다면 pass

observer을 생성하고 요소를 관찰한다.

컴포넌트가 언마운트되면 cleanup(모든 주시 해제)한다. 

ref를 리턴한다.

 

useEffect dep arr

여기서 useEffect의 의존성 배열을 살펴보자.

놀랍게도 ref는 지워도 아무 영향 없다.

왜냐하면 ref.current가 바뀌는 것이기 때문이다. ref는 persist하다.

지운다면 무엇이 useEffect를 동작시키는가?

options는 카카오 블로그에 설명한대로 영향을 줄 케이스가 적다.

그렇다면 callback이 남는다.

callback은 기본적으로 memo되고 dep arr에는 onIntersect가 있다.

// 위 예시에 적용된 onIntersect
(async (entry, observer) => {
   observer.unobserve(entry.target)
   if (hasNextPage && !isFetching) {
     fetchNextPage()
   }
 }

UsersPage 컴포넌트가 리랜더링되면 해당 함수(onIntersect)도 재생성된다.

따라서 callback의 dep Arr(onIntersect)에 영향을 주고 이에따라 useEffect의 dep Arr(callback)도 영향을 준다!

많은 사람들이 이러한 동작을 의도하지 않았을 것이다. 

그래도 이것이 오답은 아니라 생각한다. (애초에 함수 재생성으로 인한 메모리 누수를 쉽게 겪기 힘들기도 하고 저 코드는 어쨌거나 잘 돌아간다.)

난 다른식으로 IntersectionObserver을 다뤘는데, 다음포스트엔 어떻게 내가 이를 다뤘는지에 대해 파악해보자.