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 |