웹 페이지를 구현하다보면 스크롤을 통해 추가적인 정보를 보여주는 경우가 정말 많습니다.
물론 그냥 출력해도 상관없겠지만 이왕이면 다홍치마라고 조금이라도 역동적으로 정보를 출력하면 사용자 입장에서도 재밌는 UI 요소로 느끼게 할 수 있습니다.
개인 웹 포트폴리오를 구현하며 이러한 스크롤 애니매이션을 적용하고 싶었고, 관련해서 사용한 IntersectionObserver을 좀 살펴보려고 합니다.
Intersection Observer API는 타겟 요소와 상위 요소 또는 최상위 document 의 viewport 사이의 intersection 내의 변화를 비동기적으로 관찰하는 방법입니다. (MDN)
기존에 scroll 관련 이벤트는 onScroll를 활용하여 구현했습니다. 하지만 onScroll 사용 시 각 스크롤에 대해 무조건적으로 이벤트가 발생하여 성능 저하 이슈가 있었습니다. 이에 많은 개발자들이 IntersectionObserver로 눈을 돌리게 되는데, 이 API는 비동기적으로 target document가 뷰포트에 포함되는지 여부를 확인하기 때문에 성능상 더 이점을 가져갑니다.

IntersectionObserver 생성자는 두 가지의 매개변수를 받습니다.
그 후 관찰하고 싶은 DOM을 observe 함수의 매개변수로 등록하여 관찰을 시작합니다.
const observer = new IntersectionObserver(callback, options);
observer.observe(targetDOM);
entries는 IntersectionObserverEntry 인스턴스의 배열입니다.
IntersectionObserverEntry는 읽기 전용(Read only)의 다음 속성들을 포함합니다.
💡 루트 : 최상위 뷰포트
boundingClientRect: 관찰 대상의 사각형 정보를 반환합니다. (DOMRectReadOnly)intersectionRect: 관찰 대상과 루트의 교차한 영역 정보를 반환합니다.(DOMRectReadOnly)intersectionRatio: 관찰 대상의 교차한 영역을 0~1 사이의 값으로 반환합니다. 반환합니다.(intersectionRect 영역에서boundingClientRect 영역까지 비율, Number)isIntersecting: 관찰 대상이 현재 루트 안에 포함되어 있는지의 여부를 확인합니다. 현재 포함되고 있는 중이면 true, 포함되지 않고 있는 중이면 false를 반환합니다.(Boolean)rootBounds: 지정한 루트 요소의 사각형 정보를 반환압니다.(DOMRectReadOnly)target: 현재 관찰 대상 요소를 반환합니다.(Element)time: 관찰 대상의 변경이 발생한 시간 정보를 반환합니다.(DOMHighResTimeStamp)targetDOM의 가시성을 검사하기 위해 뷰포트 대신 사용할 요소 객체(루트 요소)를 지정합니다.
타켓의 조상 요소이어야 하며 지정하지 않거나 null일 경우 브라우저의 뷰포트가 기본 사용됩니다.
기본값은 null입니다.
const observer = new IntersectionObserver(callback, {
root: document.getElementById("root")
});
바깥 여백을 활용해 root의 범위를 지정합니다.
CSS의 margin과 동일하게 적용할 수 있으며, px, % 단위를 활용할 수 있습니다.
const observer = new IntersectionObserver(callback, {
rootMargin: "20px 0px 10px 5px"
});
옵저버가 실행되기 위해 타겟의 가시성이 얼마나 필요한지 백분율로 표시합니다.
기본값은 Array 타입의 [0]이지만 Number 타입의 단일 값으로도 작성할 수 있습니다.
const observer = new IntersectionObserver(callback, {
threshold: 0.3 // or `threshold: [0.3]`
})
IntersectionObserver를 React에서 활용하기 위해 저는 주로 Custom Hook를 이용했습니다.
import { useRef, useCallback, useEffect } from "react";
const useScrollEvent = (timing) => {
const dom = useRef();
const handleScroll = useCallback(([entry]) => {
const { current } = dom;
if (entry.isIntersecting) {
current.style.transitionProperty = "opacity transform";
current.style.transitionDuration = "1s";
current.style.transitionTimingFunction = "cubic-bezier(0, 0, 0.2, 1)";
current.style.transitionDelay = "0s";
current.style.opacity = 1;
current.style.transform = "translate3d(0, 0, 0)";
} else {
current.style.opacity = 0;
current.style.transform = "translate3d(0, 50%, 0)";
}
}, []);
useEffect(() => {
let observer;
const { current } = dom;
if (current) {
observer = new IntersectionObserver(handleScroll, { threshold: timing });
observer.observe(current);
return () => observer && observer.disconnect();
}
}, [handleScroll, timing]);
return {
ref: dom,
style: {
opacity: 0,
transform: "translate3d(0, 50%, 0)",
},
};
};
export default useScrollEvent;
useEffect 내에서 ref객체를 관찰하도록 설정합니다. handleScroll 콜백 함수를 IntersctionObserver에 등록해 준 후 threshold: timing를 주어 가시성 비율을 매개변수로 받아 지정합니다.
관찰 될 대상의 css의존도를 최대한 낮추기 위해 직접 주입 방법을 선택하여 return시 해당 attribute들을 컴포넌트에 바로 적용할 수 있도록 구현했습니다.
const scrollObserver = useScrollEvent(0.5);
return(
...
<MyComponent {...scrollObserver}/>
)