Intersection Observer를 활용한 모달 애니메이션 구현하기

드엔트론프·2023년 6월 5일
0

들어가며

  • 결제하기 페이지를 만드는 중, 결제 버튼이 브라우저 최하단에 갔을 때 div가 보이는 형태로 구현하고 싶었다.
  • 이전 무한스크롤을 구현할 때 사용했던 Intersection Observer가 그 기능을 충실히 해줄거라고 생각했고, 시도했다.

이 gif 처럼, 테스트박스가 스르륵 나오는것!

Intersection Observer란?

먼저 간단하게 정리하고 가자.

  • Intersection Observer API는 다음과 같은 상황에 호출되는 콜백을 생성하는 기능을 제공합니다:
    (1) 대상(target) 으로 칭하는 요소가 기기 뷰포트나 특정 요소(이 API에서 이를 root 요소 혹은 root로 칭함)와 교차함.
    (2) observer가 최초로 타겟을 관측하도록 요청받을 때마다.
  • intersection observer 를 생성하기 위해서는 생성자 호출 시 콜백 함수를 제공해야 합니다. 이 콜백 함수는 threshold가 한 방향 혹은 다른 방향으로 교차할 때 실행됩니다.
let observer = new IntersectionObserver(callback, options);

IntersectionObserver() 생성자에 전달되는 options 객체는 observer 콜백이 호출되는 상황을 조작할 수 있습니다.

let options = {
  root: document.querySelector('#scrollArea'),
  rootMargin: '0px',
  threshold: 1.0
}

아래부터는 옵션에 관한 설명이다.
설명이 길어 조금 복잡하게 느껴질 수 있으나, 각 옵션이 어떠한 역할을 하는지 알 수 있으니 관심있다면 찬찬히 보는것을 추천한다.
(해당 내용은 MDN 내용이니 믿고 봐도 된다..!)


root

대상 객체의 가시성을 확인할 때 사용되는 뷰포트 요소입니다. 이는 대상 객체의 조상 요소여야 합니다. 기본값은 브라우저 뷰포트이며, root 값이 null 이거나 지정되지 않을 때 기본값으로 설정됩니다.

컨테이너와 요소의 교차점을 추적하기 전에 그 컨테이너가 무엇인지 알아야 합니다. 그 컨테이너는 교차점 루트 또는 루트 요소입니다. 이 요소는 관찰할 요소의 조상인 문서에 있는 특정 요소이거나 컨테이너로 문서의 뷰포트를 사용하기 위해 null일 수 있습니다.

루트 교차 사각형은 대상 또는 대상과 비교하는 데 사용되는 사각형입니다. 이 사각형은 다음과 같이 결정됩니다.

  • 교차점 루트가 암시적 루트(즉, 최상위 문서)인 경우 루트 교차 사각형은 뷰포트의 사각형입니다.
  • 교차점 루트가 overflow clip을 가지고 있는 경우 루트 교차 사각형은 루트 요소의 콘텐츠 영역입니다.
  • 그 외의 경우 루트 교차 사각형은 IntersectionObserver를 생성할 때 rootMargin 속성을 설정하여 조정할 수 있습니다. rootMargin의 값은 교차점 루트의 경계 상자에 추가되는 오프셋을 정의하여 최종 교차점 루트 경계를 생성합니다

rootMargin

root 가 가진 여백입니다. 이 속성의 값은 CSS의 [margin](https://developer.mozilla.org/ko/docs/Web/CSS/margin) 속성과 유사합니다. e.g. "10px 20px 30px 40px" (top, right, bottom, left). 이 값은 퍼센티지가 될 수 있습니다. 이것은 root 요소의 각 측면의 bounding box를 수축시키거나 증가시키며, 교차성을 계산하기 전에 적용됩니다. 기본값은 0입니다.

threshold

observer의 콜백이 실행될 대상 요소의 가시성 퍼센티지를 나타내는 단일 숫자 혹은 숫자 배열입니다. Intersection Observer API는 대상 요소의 가시성이 미세하게 변경될 때마다 보고하지 않습니다. 대신, 관찰자를 생성할 때 대상 요소의 가시성 비율을 나타내는 하나 이상의 숫자 값을 제공할 수 있습니다. 그런 다음 API는 이러한 임계값을 넘는 가시성 변경만 보고합니다.

만일 50%만큼 요소가 보여졌을 때를 탐지하고 싶다면, 값을 0.5로 설정하면 됩니다. 혹은 25% 단위로 요소의 가시성이 변경될 때마다 콜백이 실행되게 하고 싶다면 [0, 0.25, 0.5, 0.75, 1] 과 같은 배열을 설정하세요. 기본값은 0이며(이는 요소가 1픽셀이라도 보이자 마자 콜백이 실행됨을 의미합니다). 1.0은 요소의 모든 픽셀이 화면에 노출되기 전에는 콜백을 실행시키지 않음을 의미합니다.

  • 대상이 IntersectionObserver에 대해 지정된 임계값(threshold) 을 충족할 때마다 콜백이 호출됩니다.
  • 콜백은 IntersectionObserverEntry(en-US) 객체 목록과 관찰자(oberver)를 수신합니다.
  • Intersection Observer API가 고려하는 모든 영역은 사각형입니다. 불규칙한 모양의 요소는 모든 요소의 모든 부분을 포함하는 가장 작은 사각형을 차지하는 것으로 간주됩니다. 마찬가지로 요소의 보이는 부분이 직사각형이 아닌 경우 요소의 교차 사각형은 요소의 모든 보이는 부분을 포함하는 가장 작은 사각형으로 간주됩니다.
  • IntersectionObserverEntry (en-US)에서 제공하는 다양한 속성이 교차점을 설명하는 방법을 이해하는 것이 유용합니다.
    • intersectionRect : 요소의 교차 사각형입니다.

    • isIntersecting : 요소가 교차하고 있는지 여부입니다.

    • intersectionRatio : 요소가 교차하는 비율입니다.

    • rootMargin : 교차 영역을 계산할 때 고려되는 루트 요소의 마진입니다.

    • boundingClientRect : 루트 요소의 사각형입니다.

    • intersectionObserverEntryIndex : 이 항목이 IntersectionObserver의 결과 목록에서 차지하는 순서입니다.

      let callback = (entries, observer) => {
        entries.forEach(entry => {
          // Each entry describes an intersection change for one observed
          // target element:
          //   entry.boundingClientRect
          //   entry.intersectionRatio
          //   entry.intersectionRect
          //   entry.isIntersecting
          //   entry.rootBounds
          //   entry.target
          //   entry.time
        });
      };
  • 예시 코드
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        if (entry.intersectionRatio >= 0.5) {
          // 요소가 뷰포트의 50% 이상을 차지합니다.
        }
      });
    });
    
    const target = document.getElementById("target");
    observer.observe(target);
    const observer = new IntersectionObserver((entries) => {
      entries.forEach((entry) => {
        console.log(entry.intersectionRatio);
      });
    });
    
    const target = document.getElementById("target");
    observer.observe(target, {
      thresholds: [0.25, 0.5, 0.75] //target 요소가 뷰포트의 25%, 50%, 75%를 차지할 때마다 콜백 함수를 호출
    });

구현방법

  1. 임의의 div를 결제페이지 최하단에 두고, useRef로 해당 div를 인식한다.
const PaymentInfor = () => {
  const observeRef = useRef(null);

  return (
    <>
      <여러 코드들 ..>

      <div ref={observeRef} />
    </>
  );
};
  1. 저 div가 보이면, 모달처럼 div를 보여줄 것이다.
const [isObserverRefFind, setIsObserverRefFind] = useState(false);	

useEffect(() => {
    const scrollObserver = new IntersectionObserver((entries) => {
      entries.forEach((entry) => setIsObserverRefFind(entry.isIntersecting)),
        {
          root: null,
          rootMargin: "0px",
          threshold: 1,
        };
    });
    if (observeRef.current) {
      scrollObserver.observe(observeRef.current);
    }
    return () => {
      if (observeRef.current) {
        // scrollObserver.unobserve(observeRef.current);
        scrollObserver.disconnect();
      }
    };
  }, []);

위 코드에서 중요하게 볼 사항은

entries.forEach((entry) => setIsObserverRefFind(entry.isIntersecting)),

이 부분이다.

감지가 되면, entry.isIntersecting 은 boolean 값으로 true 로 바뀌는데 이 값을 useState인 setIsObserverRefFind 가 받는다. 그렇기에, isObserverRefFind 처음 상태 false 에서 감지되면 true로 바뀌게 된다.

  1. 그래서 true가 되면 PaymentSlide를 보여주게 되는데, isObserverRefFind값을 넘겨주어 CSS로 처리하게된다.
return (
    <>
      <PaymentInforwrap>
        <LectureInfo>
          {isLoading ? (
            <div>신청정보 가져오는중..</div>
          ) : (
            <PaymentLectureInfo productDatas={productDatas} />
          )}
        </LectureInfo>
        <LectureInfo>
          <PaymentSelect
            radioValue={radioValue}
            setRadioValue={setRadioValue}
          />
        </LectureInfo>
      </PaymentInforwrap>
        <PaymentSlide
          isObserverRefFind={isObserverRefFind}
          productDatas={productDatas}
          radioValue={radioValue}
        />
      <div ref={observeRef} />
    </>
  );
//PaymentSlide.tsx

const PaymentSlide = ({ isObserverRefFind }) => {

 return (
    <PaymentSlideContainer className={isObserverRefFind ? "show" : "hide"}>
      <PaymentDetails>
        <PaymentDetailDiv>
		        ~~~내용~~~
        </PaymentDetailDiv>
      </PaymentDetails>
      <UserAgreeContainer>
						~~~내용~~~	      
      </UserAgreeContainer>
    </PaymentSlideContainer>
  );
};

export default PaymentSlide;

const showAnimation = keyframes`
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
`;

const hideAnimation = keyframes`
  from {
    opacity: 1;
    transform: translateY(0);
  }
  to {
    opacity: 0;
    transform: translateY(10px);
  }
`;

const PaymentSlideContainer = styled.div`
  width: 100%;
  padding-left: 6.1%;
  padding-right: 6.1%;
  background-color: #f2f2f2;
  position: fixed;
  bottom: 0;
  left: 0;
  z-index: 30;
  opacity: 0;
  transform: translateY(20px);
  animation-fill-mode: forwards;
  animation-duration: 0.3s;

  &.show {
    animation-name: ${showAnimation};
  }

  &.hide {
    animation-name: ${hideAnimation};
  }
`;
  • isObserverRefFind 의 상태에 따라 show, hide 애니메이션을 설정하여, opacity의 값을 주었다.

트러블슈팅

스르륵 등장하지만 슉-하고 사라져버리는 div

  • 사라질 때도 부드럽게 애니메이션 적용해서 사라지게 하고 싶었는데, 코드를 잘못짜서 안됐다.
  • isObserverRefFind의 값을 계속 PaymentInfor에서 있으면 보이고 아니면 빈 div를 보여주게 설정했더니 사라지는게 안됐는데, 계속 고민하다 코드 수정해서 해결할 수 있었다.

Intersection 옵저버 활용

entries.forEach((entry) => setIsObserverRefFind(entry.isIntersecting)),

~~
if (observeRef.current) {
      scrollObserver.observe(observeRef.current);
    }
  • 이 부분에서도 어떻게 state값을 주지 하면서, if 문에 주고 하다가 결국 찾아낸 방법이었다!

배운점

  • intersection observer api는 활용할수록 좋은 기능이라고 생각한다.화면 비율이 줄어들고 늘어들고간에 알아서 확인을 해주니 이만큼 편한 기능도 없는것 같기도 하다.
  • 다시봐도 어렵지만, 다음번엔 이번 시도를 통해 더 빠르게 구현할 수 있을 것 같다.

출처
https://developer.mozilla.org/ko/docs/Web/API/Intersection_Observer_API

profile
왜? 를 깊게 고민하고 해결하는 사람이 되고 싶은 개발자

0개의 댓글