1. scroll 이벤트를 감지한다. 2. 현재 스크롤 영역의 `위치를 계산`한다. with Throttle 3. 영역 계산을 통해 페이지 아래에 위치하면 API 요청을 진행한다. 4. 받아온 데이터를 추가하여 다시 렌더링한다. 5. 무한 반복 const handleScroll = () => { const { scrollTop, offsetHeight } = document.documentElement if (window.innerHeight + scrollTop >= offsetHeight) { setFetching(true) } }
쓰로틀을 걸어서 실질적인 이벤트감지 수를 최적화하더라도 documentElement.scrollTop
과 documentElement.offsetHeight
는 리플로우
(Reflow)가 발생한다.
Reflow란?
레이아웃 계산을 다시 하는 것으로, `Reflow`가 발생하면 `Repaint`는 필연적으로 발생한다.
리플로우는 HTML 요소들의 위치와 크기를 다시 계산해야 하기 때문에, 리페인트에 비해서 시간이 오래걸린다.
즉, 변경하려는 특정 요소의 위치와 크기뿐 아니라, 연관된 다른 요소들의 위치와 크기까지 재계산해야 하기 때문이다. 따라서
**리플로우가 자주 발생하도록 하는 코드는 지양해야한다.**
브라우저는 Viewport
와 Target
으로 설정한 요소의 교차점을 관찰 및 Target
이 Viewport
에 포함되는지 구별하는 기능 제공한다.
Intersection Observer의 옵션값으로는 아래와 같음.
interface IntersectionObserverInit { root?: Element | Document | null; rootMargin?: string; threshold?: number | number[]; } new IntersectionObserver(callback, options: IntersectionObserverInit)
target
의 가시성을 확인할 때 사용되는 상위 속성 이름, null
입력 시, 기본값으로 브라우저의 Viewport
가 설정된다.
root
에 마진값을 주어 범위를 확장 가능하다.
0px 0px 0px 0px
이며, 반드시 단위 입력 필요콜백이 실행되기 위해 target
의 가시성이 얼마나 필요한지 백분율로 표시.
Number
타입의 단일 값으로도 작성 가능하다.// useIntersect.ts import { RefObject, useEffect, useState } from 'react'; export const useIntersect = (ref: RefObject<HTMLDivElement>, options) => { const [observerEntry, setObserverEntry] = useState<IntersectionObserverEntry | null>(null); useEffect(() => { if (!ref?.current) return; const observer = new IntersectionObserver(([entry: IntersectionObserverEntry]) => { setObserverEntry(entry); if (entry.isIntersecting) { observer.unobserve(entry.target); } }, options); observer.observe(ref.current); return () => observer.disconnect(); }, [ref, options.rootMargin]); return observerEntry; }; // lib.dom.d.ts interface IntersectionObserverEntry { readonly boundingClientRect: DOMRectReadOnly; readonly intersectionRatio: number; readonly intersectionRect: DOMRectReadOnly; readonly isIntersecting: boolean; readonly rootBounds: DOMRectReadOnly | null; readonly target: Element; readonly time: DOMHighResTimeStamp; }
먼저 반환할 entry
를 상태로 관리하고 커스텀훅으로 전달받은 ref, rootMargin
이 변경될 때 마다 새로운 observer
를 생성한다.
원래 observer
의 target
을 SpaceGrid
의 바로 밑에 1px
의 div
를 만들어서 바라보게 하는 방식으로 구현했었는데 그려지는 스페이스의 마지막 썸네일을 기준으로 처리하면 좋을 것 같다는 생각으로 위와 같이 처리했다.
그리고 useIntersect
가 언마운트될때는 observer.disconnect()
를 해준다.
리액트 컴포넌트에 대해 공부하다보니 컴포넌트간의 의존성을 최대한 줄이는 것이 좀 더 나은 방법이라는 생각이 들었다.
카드 컴포넌트의 썸네일을 forwardRef
로 커스텀훅의 ref
로 넘겨서 별도의 로직에서 처리하다보니 상위 컴포넌트에서 자식컴포넌트간의 의존성이 추가됐고 어쩌면 내가 생각한 첫번째 구현 방식이 맞았다는 생각이 들었다.
하지만 기존에 구현하려던 SpaceGrid
하위에 1px
의 가짜 div
를 사용한다면 미세하더라도 디자이너의 요구사항에 1px
이 맞지 않는 상황이 발생할 수 있다.
이에 대해 찾아보니 1px
이 아닌 0px
의 div
를 사용하더라도 intersectionObserver api
의 root
는 해당 div
를 감지할 수 있다.
결론적으로 SpaceGrid
아래에 가짜 div
를 통해 배열을 갱신하여 ref
와 카드 컴포넌트의 썸네일의 의존성을 없애고 카드 컴포넌트는 자신의 역할에 맞게 그냥 SpaceGrid
가 하나씩 내려주는 스페이스의 정보만 카드로 보여주도록 처리할 수 있었다.