@cp949/mui
Version:
CP949 MUI React component library
146 lines (133 loc) • 5 kB
text/typescript
import { useCallback, useEffect, useMemo, useState } from 'react';
import { BehaviorSubject, skip, throttleTime } from 'rxjs';
import { useIsomorphicEffect } from './useIsomorphicEffect.js';
import { useWindowSize } from './useWindowSize.js';
/**
* DOM 요소의 절대 위치(문서 기준)를 추적하는 React 훅입니다.
* ResizeObserver와 윈도우 크기 변화를 감지하여 요소의 위치를 실시간으로 업데이트합니다.
*
* 이 훅은 스크롤 위치와 관계없이 문서 전체에서의 절대 좌표를 반환하며,
* RxJS의 BehaviorSubject와 throttleTime을 사용하여 성능을 최적화합니다.
*
* @template E - 추적할 DOM 요소의 타입 (기본값: Element)
* @param deps - 위치 재계산을 트리거할 의존성 배열
* @param options - 훅의 동작을 제어하는 옵션 객체
* @param options.intervalMs - 위치 업데이트 간격 (밀리초, 기본값: 0)
* @returns [ref 함수, 위치 객체] 튜플
* - ref 함수: 추적할 요소에 연결할 ref 함수
* - 위치 객체: { x: number, y: number } 형태의 절대 좌표
*
* @example
* ```tsx
* const Component = () => {
* const [elementRef, position] = useAbsolutePosition([], { intervalMs: 100 });
*
* return (
* <div>
* <div ref={elementRef}>추적 대상 요소</div>
* <p>절대 위치: x={position.x}, y={position.y}</p>
* </div>
* );
* };
* ```
*/
export function useAbsolutePosition<E extends Element = Element>(
deps: any[],
options?: {
/** 위치 업데이트 간격 (밀리초) @default 0 */
intervalMs?: number;
},
): [(element: E | null) => void, { x: number; y: number }] {
const [element, ref] = useState<E | null>(null);
const { intervalMs = 0 } = options || {};
const [refreshToken, setRefreshToken] = useState(0);
const [position, setPosition] = useState({ x: 0, y: 0 });
const pendingPosition$ = useMemo(
() => new BehaviorSubject<{ x: number; y: number } | null>(null),
[],
);
const { width: windowWidth, height: windowHeight } = useWindowSize();
const observer = useMemo(
() =>
typeof window === 'undefined'
? null
: new window.ResizeObserver((entries) => {
if (entries[0]) {
// const { x, y, width, height, top, left, bottom, right } = entries[0].contentRect;
// setRect({ x, y, width, height, top, left, bottom, right });
setRefreshToken(Date.now());
}
}),
[],
);
useIsomorphicEffect(() => {
if (!observer) return;
if (!element) return;
observer.observe(element);
return () => {
observer.disconnect();
};
}, [element, observer]);
const updatePosition = useCallback((newPos: { x: number; y: number }) => {
setPosition((prev) => {
if (fuzzyEquals(prev.x, newPos.x) && fuzzyEquals(prev.y, newPos.y)) {
return prev;
}
return newPos;
});
}, []);
useIsomorphicEffect(() => {
if (!element) {
updatePosition({ x: 0, y: 0 });
return;
}
pendingPosition$.next(getAbsolutePosition(element));
}, [pendingPosition$, refreshToken, element, windowWidth, windowHeight, updatePosition, ...deps]);
useEffect(() => {
const s1 = pendingPosition$
.pipe(skip(1), throttleTime(intervalMs))
.subscribe((pendingPosition) => {
if (pendingPosition) {
updatePosition(pendingPosition);
}
});
return () => {
s1.unsubscribe();
};
}, [pendingPosition$, intervalMs, updatePosition]);
return [ref, position];
}
/**
* 두 숫자가 거의 같은지 비교하는 유틸리티 함수입니다.
* 부동소수점 연산의 정밀도 문제를 해결하기 위해 사용됩니다.
*
* @param a - 비교할 첫 번째 숫자
* @param b - 비교할 두 번째 숫자
* @param epsilon - 허용 오차 (기본값: 0.0001)
* @returns 두 숫자의 차이가 epsilon보다 작으면 true
*/
function fuzzyEquals(a: number, b: number, epsilon = 0.0001): boolean {
return Math.abs(a - b) < epsilon;
}
/**
* DOM 요소의 문서 기준 절대 위치를 계산하는 함수입니다.
* getBoundingClientRect()와 스크롤 위치를 조합하여 절대 좌표를 구합니다.
*
* @param element - 위치를 계산할 DOM 요소
* @returns 문서 기준 절대 좌표 { x: number, y: number }
*/
function getAbsolutePosition(element: Element): { x: number; y: number } {
if (typeof window === 'undefined') {
// ssr
return { x: 0, y: 0 };
}
// 요소의 bounding rectangle을 가져옵니다.
const rect = element.getBoundingClientRect();
// 페이지의 스크롤 위치를 가져옵니다.
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;
// 절대 위치를 계산합니다.
const top = rect.top + scrollTop;
const left = rect.left + scrollLeft;
return { x: left, y: top };
}