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
| /* eslint-disable @typescript-eslint/no-explicit-any */ | |
| import { useCallback, useEffect, useRef } 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); | |
| }; | |
| } | |
| // useIntersectionObserver 훅은 Intersection Observer를 사용하여 요소의 가시성을 감지합니다. | |
| // The useIntersectionObserverV2 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 = 100, // 기본 초기 지연 시간을 100ms로 설정 / Default initial delay time set to 100ms | |
| $callbackDebounce = 100, // 기본 콜백 디바운스 시간을 100ms로 설정 / Default callback debounce time set to 100ms | |
| ...options | |
| } = {}, | |
| } = props; | |
| const onceStore = useRef(new Map<HTMLElement, boolean>()); // 한 번만 감지된 요소를 저장 / Store 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; | |
| } | |
| }, []); | |
| // 관찰 토글 함수: 현재 타겟 요소의 관찰을 토글합니다. | |
| // 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); | |
| } | |
| }; | |
| // Intersection Observer 설정 | |
| // Setting up the Intersection Observer | |
| useEffect(() => { | |
| observer.current = new IntersectionObserver( | |
| (entries) => { | |
| entries.forEach((entry) => { | |
| if (onceStore.current.get(entry.target as HTMLElement)) { | |
| return; | |
| } | |
| if (entry.isIntersecting) { | |
| visibleElements.current.add(entry.target); | |
| if ($once) { | |
| onceStore.current.set(entry.target as HTMLElement, true); | |
| } | |
| } else { | |
| visibleElements.current.delete(entry.target); | |
| } | |
| debouncedEntryFuncs.current.get(entry.target)?.(entry); | |
| }); | |
| }, | |
| { threshold: 0.7, ...options }, | |
| ); | |
| setTimeout(() => { | |
| 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)) { | |
| callbackProps(entry); | |
| if (process.env.NODE_ENV === 'development') { | |
| console.log('[DEV] intersection ', entry); | |
| } | |
| } | |
| }, $callbackDebounce); | |
| debouncedEntryFuncs.current.set(element, entryCalled); | |
| }); | |
| } else { | |
| subscribe(target.current); | |
| } | |
| }, $initDelay); | |
| return () => { | |
| observer.current?.disconnect(); | |
| observer.current = null; | |
| }; | |
| }, [callbackProps, options]); | |
| return [subscribe, unsubscribe, toggleObserve] as const; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment