useIntersectionObserverRef 커스텀 훅 만들기

2023. 2. 16. 11:17·프론트엔드/React

IntersectionObserver는 무한 스크롤 or 이미지 lazy loading 등 다양한 곳에서 활용할 수 있다.

react, typescript 환경에서 쉽게 IntersectionObserver를 활용할 수 있는 커스텀 훅을 제작했다.

Typing

interface UseIntersectionObserverRefProps {
  readonly callback: IntersectionObserverCallback
  readonly options?: IntersectionObserverInit
  readonly type?: 'callback' | 'ref'
}

export const useIntersectionObserverRef = <T extends HTMLElement>({
  callback,
  options = { root: null, rootMargin: '0px', threshold: 0 },
  type = 'ref',
}: UseIntersectionObserverRefProps): RefCallback<T> | RefObject<T> => {
    /// 생략
}

props 는 해당 DOM을 교차할 때 실행되는 callback과 observer의 기준 컨테이너, 교차 비율 등을 커스텀할 수 있는 options, ref를 refCallback 으로 받을지 refObject로 받을지 결정하는 type으로 구성했다.

함수 타입은 제네릭을 활용해 <T extends HTMLElement> HTMLElement의 타입별로 활용할 수 있게 했다. 리턴 타입은 RefCallback<T> | RefObject<T>로 줘서 type에 따라 ref 콜백 or ref 오브젝트를 주도록 했다.

콜백 포장하기

const callbackOnlyIntersecting: IntersectionObserverCallback = (entries, observer) => {
    const isIntersecting = entries.map(entry => entry.isIntersecting).reduce((acc, cur) => acc && cur, true)
    if (isIntersecting) {
      callback(entries, observer)
    }
  }

IntersectionObserver는 생성될 때 callback을 실행한다. 그래서 생성될 때 실행을 막기 위해서 콜백을 포장했다. 콜백에 인자로 전달되는 entries의 각 원소는 실제로 화면에서 교차하고 있는지 알려주는 isIntersecting이라는 프로퍼티를 가진다. 이 프로퍼티를 활용해서 실제로 관찰중인 DOM이 교차할 때만 콜백이 실행되도록 했다.

Observer로 DOM 관찰시키기

단일 타겟

하나의 DOM 노드만 관찰할 필요가 있는 경우(ex: 무한 스크롤) 에는 type='ref' (default) 로 훅을 호출하여 사용하면 된다.

const observerRef = useRef<IntersectionObserver>(new IntersectionObserver(callbackOnlyIntersecting, options))
const elementRef = useRef<T>(null)

useEffect(() => {
if (type === 'callback' || !elementRef.current || !observerRef.current) {
  return
}
observerRef.current.observe(elementRef.current)
return () => observerRef.current.disconnect()
}, [elementRef, observerRef])
return elementRef

IntersectionObserver를 생성하고 dom에 연결된 ref를 활용해 해당 dom을 관찰시킨다. 언마운트시 관찰을 해제한다.

생성된 elementRef를 리턴한다.

배열 타겟

여러개의 DOM 노드를 관찰할 필요가 있는 경우(ex: 이미지 레이지로딩)에는 type='callback'로 훅을 호출한다.

if (type === 'callback') {
  const refCallback = (element: T) => {
      if (element && observerRef.current) {
      observerRef.current.observe(element)
      }
  }
return refCallback
}

리액트에서 DOM 노드에 ref에 콜백함수를 전달해서 IntersectionObserver를 사용할 수 있다. 그 경우에 refCallback: (el: T extends HTMLElement) => void 를 DOM의 ref로 주면 된다. 이를 이용해서 여러개의 노드를 타겟으로 할 떄 ref를 많이 만들 필요 없이 콜백 한개로 처리할 수 있다.

전체 코드

import { Ref, RefCallback, RefObject, useEffect, useRef } from 'react'

interface UseIntersectionObserverRefProps {
  readonly callback: IntersectionObserverCallback
  readonly options?: IntersectionObserverInit
  readonly type?: 'callback' | 'ref'
}

export const useIntersectionObserverRef = <T extends HTMLElement>({
  callback,
  options = { root: null, rootMargin: '0px', threshold: 0 },
  type = 'ref',
}: UseIntersectionObserverRefProps): RefCallback<T> | RefObject<T> => {
  const callbackOnlyIntersecting: IntersectionObserverCallback = (entries, observer) => {
    const isIntersecting = entries.map(entry => entry.isIntersecting).reduce((acc, cur) => acc && cur, true)
    if (isIntersecting) {
      callback(entries, observer)
    }
  }
  const observerRef = useRef<IntersectionObserver>(new IntersectionObserver(callbackOnlyIntersecting, options))
  const elementRef = useRef<T>(null)

  useEffect(() => {
    if (type === 'callback' || !elementRef.current || !observerRef.current) {
      return
    }
    observerRef.current.observe(elementRef.current)
    return () => observerRef.current.disconnect()
  }, [elementRef, observerRef])

  if (type === 'callback') {
    const refCallback = (element: T) => {
      if (element && observerRef.current) {
        observerRef.current.observe(element)
      }
    }
    return refCallback
  }

  return elementRef
}

사용법

useIntersectionObserverRef훅을 활용해 간단한 무한스크롤을 구현한 예시이다.

import { useIntersectionObserverRef } from '@/hooks/useIntersectionObserver'
import { useState } from 'react'

function App() {
  const [items, setItems] = useState<any[]>([])
  const infiniteScrollRef = useIntersectionObserverRef<HTMLDivElement>({
    callback: () => {
      setItems(prev => [...prev, ...new Array(5).fill(0)])
    },
  })
  return (
    <div className="App">
      {items.map((item, idx) => (
        <div key={idx} style={{ width: '100%', height: '200px', background: 'blue', border: '1px solid white' }} />
      ))}
      <div ref={infiniteScrollRef} />
    </div>
  )
}

export default App

최하단에 위치한 div에 ref를 줘서 해당 div가 화면에 교차할 때마다 setItems를 호출한다. 즉, 스크롤해서 맨 밑으로 내릴 때마다 callback이 호출되어서 무한스크롤로 아이템들이 나타난다. 실제 활용시에는 data fetch후에 setItems를 해주는 식으로 활용할 수 있다.

저작자표시 (새창열림)

'프론트엔드 > React' 카테고리의 다른 글

웹 접근성  (0) 2024.03.17
리액트 프로젝트 폴더 구조  (0) 2023.04.08
Portal  (0) 2022.07.06
커스텀 훅 적용하기 - 팝업(모달)  (0) 2022.07.02
HOC  (1) 2022.06.04
'프론트엔드/React' 카테고리의 다른 글
  • 웹 접근성
  • 리액트 프로젝트 폴더 구조
  • Portal
  • 커스텀 훅 적용하기 - 팝업(모달)
정현우12
정현우12
  • 정현우12
    정현우의 개발 블로그
    정현우12
  • 전체
    오늘
    어제
    • 분류 전체보기 (79)
      • 프론트엔드 (56)
        • JavaScript, TypeScript (12)
        • 스타일링 (1)
        • React (13)
        • Next.js (4)
        • 개발 환경 (9)
        • 테스트 (3)
        • 성능 최적화 (11)
        • 함수형 프로그래밍 (2)
        • 구조 (1)
      • 프로젝트 회고 (23)
        • 이미지편집기 개발 (7)
        • 엑셀 다운로드, 업로드 공통 모듈 개발 (4)
        • 사용자 매뉴얼 사이트 개발 (3)
        • 통계자동화 솔루션 개발 (1)
        • 엑셀 편집기 개발 (5)
        • API 플랫폼 (1)
        • 콜센터 솔루션 OB 캠페인 (1)
        • AI 스튜디오 (1)
      • 백엔드 (0)
  • 블로그 메뉴

    • 홈
    • 포트폴리오
    • 태그
  • 인기 글

  • 태그

    렌더링 성능 최적화
    frontend
    사용자 매뉴얼 사이트
    커스텀 훅
    웹 성능 최적화
    React-boilerplate
    JavaScript
    회고
    라이브러리 선정
    엑셀
    Next.js
    이미지 편집기
    엑셀 에디터
    로딩 성능 최적화
    webpack
    memoization
    React
    useReducer
    TypeScript
    Github Actions
  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
정현우12
useIntersectionObserverRef 커스텀 훅 만들기
상단으로

티스토리툴바