/** * useMapLayers — React 리렌더 없이 deck.gl 레이어를 업데이트하는 hook * * 원리: RAF(requestAnimationFrame) 배치로 overlay.setProps() 직접 호출 * React의 setState/render cycle을 완전 우회 */ import { useRef, useEffect } from 'react'; import type { MapboxOverlay } from '@deck.gl/mapbox'; import type { Layer } from 'deck.gl'; import type maplibregl from 'maplibre-gl'; export interface MapHandle { overlay: MapboxOverlay | null; map: maplibregl.Map | null; } /** * 레이어를 RAF 기반으로 업데이트 * @param handleRef — BaseMap의 overlay를 담은 ref * @param buildLayers — 레이어 빌드 함수 (호출될 때만 실행) * @param deps — 변경 감지 대상 (shallow 비교) */ export function useMapLayers( handleRef: React.RefObject, buildLayers: () => Layer[], deps: unknown[], ) { const prevRef = useRef([]); const rafRef = useRef(0); // deps 변경 시에만 레이어 갱신 (매 렌더 아닌 deps diff 기반) useEffect(() => { if (shallowEqual(prevRef.current, deps)) return; prevRef.current = deps; cancelAnimationFrame(rafRef.current); rafRef.current = requestAnimationFrame(() => { handleRef.current?.overlay?.setProps({ layers: buildLayers() }); }); }); // 언마운트 시에만 레이어 초기화 — stale WebGL 참조 방지 useEffect(() => { return () => { cancelAnimationFrame(rafRef.current); try { handleRef.current?.overlay?.setProps({ layers: [] }); } catch { /* finalized */ } }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); } /** * Zustand store.subscribe + RAF 기반 레이어 업데이트 * store 변경 → RAF 배치 → overlay.setProps (React 리렌더 0회) */ export function useStoreLayerSync( handleRef: React.RefObject, subscribe: (callback: (state: T) => void) => () => void, buildLayers: (state: T) => Layer[], ) { const rafRef = useRef(0); useEffect(() => { const unsub = subscribe((state) => { cancelAnimationFrame(rafRef.current); rafRef.current = requestAnimationFrame(() => { handleRef.current?.overlay?.setProps({ layers: buildLayers(state) }); }); }); return () => { unsub(); cancelAnimationFrame(rafRef.current); // 언마운트 시 레이어 초기화 — stale WebGL 참조 방지 try { handleRef.current?.overlay?.setProps({ layers: [] }); } catch { /* finalized */ } }; // buildLayers는 안정적 참조여야 함 (useCallback으로 감싸거나 모듈 스코프) // eslint-disable-next-line react-hooks/exhaustive-deps }, [handleRef, subscribe]); } function shallowEqual(a: unknown[], b: unknown[]): boolean { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (!Object.is(a[i], b[i])) return false; } return true; }