이 글은 클래스101 사이트를 클론 코딩하는 프로젝트를 하던 중 알게 된 점을 작성한 글입니다.
1년 전에 한 프로젝트지만 지금이라도 기록...
<p id="top">여기가 맨 위</p>
<a href="#top">맨 위로 가기 버튼</a>
이렇게 href="#id"
속성을 이용했었지만... 이렇게 하면 2가지가 마음에 들지 않았다.
다른 방법을 찾아보자.
이동할 각각의 컴포넌트에 ref
를 걸어주면 된다.
그런데 목차가 7개 있다고 치면 ref
를 7개 각각 따로 만들어야 할까?
하나의 ref
에 다 집어넣고 관리할 수는 없을까?
그리고, NavBar랑 이동할 각각의 컴포넌트들이랑 어떻게 ref
를 공유할 수 있을까?
ref
를 배열로 관리하면서 forwardRef()
로 자식 컴포넌트에게 넘겨주기ref
를 각각의 컴포넌트에 넘겨준다.ref
는 props
처럼 받을 수 없으므로 forwardRef()
의 두 번째 인자로 넘어온 ref
를 받는다.ref.current
의 배열에 해당 ref
를 넣어준다.// DetailMain.js
const DetailMain = () => {
const scrollRef = useRef([]); // 배열 ref를 하나 생성한다.
return (
<>
// NavBar에는 scrollRef의 배열을 props로 넘겨준다.
<DetailNav scrollRef={scrollRef} />
// 이동할 각각의 컴포넌트에 ref로 넘겨준다.
<DetailReview ref={scrollRef} />
<DetailClassDescription ref={scrollRef} />
<DetailCurriculum ref={scrollRef} />
<DetailCreator ref={scrollRef} />
<DetailCommunity ref={scrollRef} />
<DetailRefundPolicy ref={scrollRef} />
</>
);
};
// DetailReview.js
// 이동할 컴포넌트에서 forwardRef 내부 함수의 두 번째 인자로 ref를 받고 ref.current 배열에 DOM을 넣어준다.
const DetailReview = forwardRef((props, ref) => {
return (
<section ref={reviewRef => (ref.current[0] = reviewRef)}>
...
</section>
);
});
// DetailRefundPolicy.js
// 이동할 컴포넌트에서 forwardRef 내부 함수의 두 번째 인자로 ref를 받고 ref.current 배열에 DOM을 넣어준다.
const DetailRefundPolicy = forwardRef((props, ref) => {
return (
<section ref={refundRef => (ref.current[5] = refundRef)}>
...
</section>
);
});
// DetailNav.js
const DETAIL_NAV = [
{ idx: 0, name: '후기' },
{ idx: 1, name: '클래스 소개' },
{ idx: 2, name: '커리큘럼' },
{ idx: 3, name: '크리에이터' },
{ idx: 4, name: '커뮤니티' },
{ idx: 5, name: '환불 정책' },
{ idx: 6, name: '추천' },
];
const DetailNav = ({ scrollRef }) => {
const [navIndex, setNavIndex] = useState(null);
const navRef = useRef([]); // 이동할 각각의 컴포넌트에 대응하는 목차 버튼을 저장할 ref 배열
useEffect(() => {
// { behavior: 'smooth' } 속성을 주면 스크롤이 스르륵~ 올라가거나 내려가면서 이동하고, 없으면 아무 애니메이션 없이 바로 목적지를 보여준다.
scrollRef.current[navIndex]?.scrollIntoView({ behavior: 'smooth' });
setNavIndex(null);
}, [scrollRef, navIndex]);
// 현재 스크롤 위치에 따라 NavBar 버튼 스타일이 바뀌도록 클래스명을 지정한다.
useEffect(() => {
const changeNavBtnStyle = () => {
scrollRef.current.forEach((ref, idx) => {
if (ref.offsetTop - 180 < window.scrollY) {
navRef.current.forEach(ref => {
ref.className = ref.className.replace(' active', '');
});
navRef.current[idx].className += ' active';
}
});
};
window.addEventListener('scroll', changeNavBtnStyle);
return () => {
window.removeEventListener('scroll', changeNavBtnStyle);
};
}, [scrollRef]);
return (
<nav>
{DETAIL_NAV.map(({ idx, name }) => (
<NavBtn
key={idx}
ref={ref => (navRef.current[idx] = ref)}
onClick={() => {
setNavIndex(idx);
}}
>
{name}
</NavBtn>
))}
</nav>
);
};
const NavBtn = styled.button`
&.active {
border-color: ${theme.black};
color: ${theme.black};
font-weight: bold;
}
`;
참고
React 공식 문서: Forwarding Refs
React 특정 DOM으로 스크롤 이동시키기
스크롤 이동에 따라 nav 스타일 변경 - CodePen