원리
현재 영역의 절반 이상을 지난 시점을 감시 영역의 경계선으로 설정한다.
아래 코드와 같이 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",
});
}
});
원리
Observer
가 배열인 ref.current
를 관찰하여 교차 이벤트 발생시 entrie
에 저장된 ref.current
를 viewIndex
로 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로 설정이 된다.
import React, { forwardRef } from "react";
const Content = ({ page }, ref) => <div ref={ref}>{page}</div>;
export default forwardRef(Content);