scroll spy

js·2022년 5월 20일
0

JS

원리

현재 영역의 절반 이상을 지난 시점을 감시 영역의 경계선으로 설정한다.

아래 코드와 같이 offsetTop, clientHeight로 각 영역의 경계위치를 설정한다.

offsetTop - (clientHeight / 2), offsetTop + (clientHeight / 2)

현재 스크롤 위치가 해당 되는 영역의 index를 찾고, 그 index에 해당되는 영역이면 on 클래스 추가 , 아니면 on 클래스 제거한다.

코드 분석

const navElem = document.querySelector("#nav");
const navItems = Array.from(navElem.children);
const contentsElem = document.querySelector("#contents");
const contentItems = Array.from(contentsElem.children);
const offsetTops = contentItems.map((elem) => {
  const [ofs, clh] = [elem.offsetTop, elem.clientHeight];
  return [ofs - clh / 2, ofs + clh / 2];
});

window.addEventListener("scroll", (e) => {
  const { scrollTop } = e.target.scrollingElement;
  // 현재 스크롤 위치가 해당 되는 영역의 index를 찾고 
  const targetIndex = offsetTops.findIndex(([from, to]) => (
    scrollTop >= from && scrollTop < to
  ))
  // 그 index에 해당되는 영역이면 on 클래스 추가 , 아니면 on 클래스 제거
  Array.from(navElem.children).forEach((c, i) => {
    if (i !== targetIndex) c.classList.remove('on');
    else c.classList.add('on');
  });
});

navElem.addEventListener("click", (e) => {
  const targetElem = e.target;
  if (targetElem.tagName === "BUTTON") {
    const targetIndex = navItems.indexOf(targetElem.parentElement);
    contentItems[targetIndex].scrollIntoView({
      block: "start",
      behavior: "smooth",
    });
  }
});

REACT

원리

Observer가 배열인 ref.current를 관찰하여 교차 이벤트 발생시 entrie에 저장된 ref.currentviewIndex로 state를 변경 해준다.

코드 분석

import React, { useState, useRef, useEffect } from "react";
import Nav from "./Nav";
import Content from "./Content";
import "./style.css";

const pages = Array.from({ length: 8 }).map((_, i) => i + 1);

const App = () => {
  const [viewIndex, setViewIndex] = useState(0);
  const contentRef = useRef([]);
  const moveToPage = index => () => {
    contentRef.current[index].scrollIntoView({
      block: "start",
      behavior: "smooth",
    });
  };

  const scrollSpyObserver = new IntersectionObserver(
    entries => {
      // 교차 중인 타겟을 찾아서 그 타겟의 인덱스 값을 현재 상태로 업데이트 
      const { target } = entries.find((entry) => entry.isIntersecting) || {};
      const index = contentRef.current.indexOf(target);
      setViewIndex(index);
    },
    {
      root: null,
      rootMargin: "0px",
      // 교차영역을 .5로 설정
      threshold: 0.5
    }
  );

  useEffect(() => {
    contentRef.current.forEach((item) => scrollSpyObserver.observe(item));
    return () => {
      contentRef.current.forEach((item) => scrollSpyObserver.unobserve(item));
    };
  }, []);

  return (
    <div id="app">
      <Nav pages={pages} viewIndex={viewIndex} moveToPage={moveToPage} />
      <div id="contents">
        {pages.map((p, i) => (
          // useRef
          <Content key={p} ref={(r) => (contentRef.current[i] = r)} page={p} />
        ))}
      </div>
    </div>
  );
};

export default App;

컴포넌트를 forwardRef로 감싸면 해당 컴포넌트의 두번째 매개변수를 가지게 되는데 , 이를 통해 외부로부터 ref를 받아 올 수 있다.

컴포넌트를 forwardRef로 감싸지 않으면 ref는 prop이 아니라서 undefined로 설정이 된다.

forwardRef 참조

import React, { forwardRef } from "react";

const Content = ({ page }, ref) => <div ref={ref}>{page}</div>;

export default forwardRef(Content);

0개의 댓글