Last active
June 10, 2024 04:31
-
-
Save minhoyooDEV/35fa34a16b5ddfc1ba55728b645c478c to your computer and use it in GitHub Desktop.
a Intersection observer with debounced callback
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useCallback, useEffect, useRef, useState } from 'react'; | |
// debounce 함수는 주어진 함수가 특정 시간 동안 호출되지 않도록 합니다. | |
// The debounce function ensures that the provided function is not called repeatedly within the specified wait time. | |
function debounce<T extends (...args: any[]) => void>( | |
func: T, | |
wait: number, | |
): (...args: Parameters<T>) => void { | |
let timeout: ReturnType<typeof setTimeout> | null = null; | |
return (...args: Parameters<T>): void => { | |
if (timeout !== null) { | |
clearTimeout(timeout); | |
} | |
timeout = setTimeout(() => { | |
func(...args); | |
}, wait); | |
}; | |
} | |
// IntersectionObserver의 상태를 나타내는 열거형(enum)입니다. | |
// Enum representing the status of the Intersection Observer. | |
enum IntersectionObserverStatus { | |
IDLE, | |
ACTIVE, | |
} | |
// useIntersectionObserver 훅은 Intersection Observer를 사용하여 요소의 가시성을 감지합니다. | |
// The useIntersectionObserver hook uses Intersection Observer to detect the visibility of elements. | |
export function useIntersectionObserver<T extends HTMLElement>(props: { | |
callback: (entry: IntersectionObserverEntry) => void; // 요소가 보일 때 실행되는 콜백 함수 / Callback function executed when the element is visible | |
options?: IntersectionObserverInit & { | |
$once?: boolean; // 요소가 한번만 감지되도록 설정 / Option to detect the element only once | |
$htmlSelector?: string; // 감지할 요소의 선택자 / Selector for the elements to be observed | |
$initDelay?: number; // 초기 지연 시간 / Initial delay time | |
$callbackDebounce?: number; // 콜백 디바운스 시간 / Callback debounce time | |
}; | |
}) { | |
const { | |
callback: callbackProps, | |
options: { | |
$htmlSelector, | |
$once, | |
$initDelay = 1600, // 기본 초기 지연 시간을 1600ms로 설정 / Default initial delay time set to 1600ms | |
$callbackDebounce = 1200, // 기본 콜백 디바운스 시간을 1200ms로 설정 / Default callback debounce time set to 1200ms | |
...options | |
} = {}, | |
} = props; | |
const [status, setStatus] = useState(IntersectionObserverStatus.IDLE); // Intersection Observer의 현재 상태를 저장 / Stores the current status of the Intersection Observer | |
const onceStore = useRef(new Map<HTMLElement, boolean>()); // 한 번만 감지된 요소를 저장 / Stores elements detected only once | |
const target = useRef<T | null>(null); // 관찰할 타겟 요소 / Target element to be observed | |
const observer = useRef<IntersectionObserver | null>(null); // Intersection Observer 인스턴스 / Intersection Observer instance | |
const visibleElements = useRef(new Set()); // 현재 보이는 요소들 / Currently visible elements | |
const debouncedEntryFuncs = useRef( | |
new Map<Element, (entry: IntersectionObserverEntry) => void>(), // 디바운스된 콜백 함수들 / Debounced callback functions | |
); | |
// 구독 함수: 노드를 관찰합니다. | |
// Subscribe function: observes the node. | |
const subscribe = useCallback( | |
(node: T | null) => { | |
if (node) { | |
observer.current?.observe(node); | |
target.current = node; | |
} | |
}, | |
[observer.current], | |
); | |
// 구독 해제 함수: 모든 관찰을 중지합니다. | |
// Unsubscribe function: stops all observations. | |
const unsubscribe = useCallback(() => { | |
if (observer.current) { | |
observer.current.disconnect(); | |
observer.current = null; | |
target.current = null; | |
} | |
}, [observer.current]); | |
// 관찰 토글 함수: 현재 타겟 요소의 관찰을 토글합니다. | |
// Toggle observe function: toggles observation of the current target element. | |
const toggleObserve = () => { | |
if (target.current) { | |
observer.current?.unobserve(target.current); | |
} else { | |
observer.current?.observe(target.current as unknown as HTMLElement); | |
} | |
}; | |
// 초기 지연 후 상태를 ACTIVE로 설정 | |
// Set the status to ACTIVE after the initial delay | |
useEffect(() => { | |
setTimeout(() => { | |
setStatus(IntersectionObserverStatus.ACTIVE); | |
}, $initDelay); | |
}, []); | |
// Intersection Observer 설정 | |
// Setting up the Intersection Observer | |
useEffect(() => { | |
if (status === IntersectionObserverStatus.IDLE) { | |
return; | |
} | |
observer.current = new IntersectionObserver( | |
(entries) => { | |
entries.forEach((entry) => { | |
const target = entry.target as HTMLElement; | |
entry.isIntersecting | |
? visibleElements.current.add(target) | |
: visibleElements.current.delete(target); | |
if (visibleElements.current.has(target)) { | |
if (onceStore.current.get(target)) { | |
return; | |
} | |
debouncedEntryFuncs.current.get(target)?.(entry); | |
} | |
}); | |
}, | |
{ threshold: 0.7, ...options }, | |
); | |
if ($htmlSelector) { | |
const elements = document.querySelectorAll($htmlSelector); | |
elements.forEach((element) => { | |
subscribe(element as T); | |
const entryCalled = debounce((entry: IntersectionObserverEntry) => { | |
if (visibleElements.current.has(entry.target)) { | |
if ($once) { | |
onceStore.current.set(entry.target as HTMLElement, true); | |
} | |
callbackProps(entry); | |
} | |
}, $callbackDebounce); | |
debouncedEntryFuncs.current.set(element, entryCalled); | |
}); | |
} else { | |
subscribe(target.current); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment