이 글은 뷰포트 내에 Element의 교차점을 관찰하여 애니메이션 동작 여부를 결정할 때 사용하기 적합한 IntersectionObserver custom hook을 간단하게 설명하고 제공하는 목적에 포커스가 되어있습니다.
우리는 IntersectionObserver를 아래와 같은 상황에 자주 사용한다.
위와같이 요소가 뷰포트에 포함되는지 아닌지, 사용자 화면에 지금 보이는 요소인지 아닌지를 구별하는 기능을 기본적으로 제공한다.
이러한 기능들은 MDN에 나와있듯이 비동기적으로 실행되기 때문에 scroll 같은 이벤트 기반 요소 관찰에서 발생하는 렌더링 성능이나 이벤트 연속 호출같은 문제 없이 사용할 수 있는 장점을 가지고 있다.
어떤 장점이 있고 이런 api가 등장하게 된 배경은 무엇인지 간단하게만 알아보자.
보통 위와같은 상황을 구현하기 위한 방법으로 scroll 이벤트를 먼저 떠올린다. 특정 엘리먼트나 document에 스크롤 이벤트를 등록하고, 특정 지점을 관찰하여 엘리먼트가 위치에 도달하면 실행할 콜백함수를 등록한다.
scroll 이벤트는 단시간에 많은 호출을 야기할 수 있고 동기적으로 실행되기 때문에 메인스레드 영향을 준다. 또한 한 페이지 내에 여러개의 scroll event 가 등록되어 있다면 사용자가 스크롤할 때마다 이를 감지하는 이벤트가 끊임없이 호출된다.
passive: true
를 통해 컴포지터 스레드가 메인 스레드의 프로세스를 기다리지 않고 자신의 작업인 paint와 composite를 즉시 수행하여 이벤트를 수신하는 즉시 핸들러를 실행하게 하여 화면에 바로 반영 및 합성 시킬 수 있게 함으로써 성능을 향상시켜 어느정도 해소할 수는 있다.특정 지점을 관찰하기 위해서는 getBoundingClientRect()
함수가 유용하게 쓰인다.
하지만 이 함수는 리플로우를 야기한다.
Intersection Observer API
를 사용하면 위와 같은 문제들을 해결할 수 있다.
비동기적으로 실행하기 떄문에 메인 스레드에는 영향을 주지 않으면서 변경사항을 관찰할 수 있다.
또한 IntersectionObserverEntry
의 속성을 활용하면 getBoundingClientRect()
를 호출한 것과 같은 결과를 알 수 있기 때문에
따로 getBoundingClientRect()
함수를 호출할 필요가 없어 리플로우 현상을 방지할 수 있다.
자 그럼 간단한 설명은 여기까지 하고 뷰포트 내에 Element의 교차점을 관찰하여 애니메이션 동작 여부를 결정할 때 사용하기 적합한 IntersectionObserver를 custom hook으로 만들어 보자.
// 구현부
import { useEffect, useState } from 'react';
interface Props extends IntersectionObserverInit {
onIntersect: IntersectionObserverCallback;
triggerOnce?: boolean;
}
const DEFAULT_THRESHOLD = 1.0;
export const useIntersectionObserver = ({ root, rootMargin = '0px', threshold = DEFAULT_THRESHOLD, onIntersect, triggerOnce = false }: Props) => {
const [target, setTarget] = useState<HTMLElement | null | undefined>(null);
useEffect(() => {
if (!target) return;
let observer: IntersectionObserver;
if (triggerOnce) observer = new IntersectionObserver(onIntersectOnce(onIntersect, triggerOnce, target), { root, rootMargin, threshold });
else observer = new IntersectionObserver(onIntersect, { root, rootMargin, threshold });
observer.observe(target);
return () => {
if (target) observer.unobserve(target);
};
}, [root, rootMargin, onIntersect, threshold, target, triggerOnce]);
return [setTarget];
};
const onIntersectOnce = (callback: IntersectionObserverCallback, triggerOnce: boolean, target: HTMLElement) => {
return (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => {
callback(entries, observer);
if (target && triggerOnce && !!entries?.some((entry) => entry.isIntersecting)) {
observer.unobserve(target);
}
};
};
// 호출부
...
const [animating, setAnimating] = useState(false);
const [setTargetRef] = useIntersectionObserver({
threshold: 0.2,
triggerOnce: true,
onIntersect: useCallback(([{ isIntersecting }]) => {
if (isIntersecting) setAnimating(true);
}, []),
});
...
<Text ref={setTargetRef} startAnimation={animating} >
애니메이션이 적용되는 컨텐츠
</Text>
보통 널리 사용하는 IntersectionObserver hook과 다른점이 있다면 크게 두가지이다.
https://www.npmjs.com/package/react-intersection-observer 패키지를 사용하는것도 추천한다.
번들사이즈도 작고 유용한 기능들이 다수 탑재되어 있어서 쉽게 사용 가능하다.
여담으로 오늘의집 사이트를 번들스캐너로 패키지 분석해봤는데 해당 패키지를 사용하고 있다.
저명한 기업이 사용하고 있다는것은 유용한 라이브러리로 검증이 됐다는 의미이지 않을까..