React | 스크롤 이동 NavBar 버튼 구현하기 (ref 활용)

설탕·2023년 1월 20일
5
post-thumbnail

이 글은 클래스101 사이트를 클론 코딩하는 프로젝트를 하던 중 알게 된 점을 작성한 글입니다.
1년 전에 한 프로젝트지만 지금이라도 기록...

구현할 기능

1. NavBar 버튼을 클릭하면 페이지 내에서 해당 DOM 위치로 스크롤 이동하기

2. 현재 스크롤 위치의 DOM에 해당하는 NavBar 버튼 스타일 강조하기

순수 html에서 페이지 내 이동을 구현하려면

<p id="top">여기가 맨 위</p>
<a href="#top">맨 위로 가기 버튼</a>

이렇게 href="#id" 속성을 이용했었지만... 이렇게 하면 2가지가 마음에 들지 않았다.

  1. 버튼을 누르는 순간 URL 맨 뒤에 #id가 붙는다. 외부 사이트에서 접속했을 때 특정 목차로 바로가기 기능이 필요하다면 이게 유용할 수도 있겠지만 그런 기능이 필요한 페이지는 아니기에 굳이 URL을 못생기게 바꾸고 싶지 않다.
  2. 전체에서 유일해야 할 id 속성을 굳이 부여해서 유지보수 복잡하게 하고 싶지 않다.

다른 방법을 찾아보자.

버튼을 눌렀을 때 URL은 그대로 유지하면서도 특정 DOM으로 스크롤 이동하려면

이동할 각각의 컴포넌트에 ref를 걸어주면 된다.
그런데 목차가 7개 있다고 치면 ref를 7개 각각 따로 만들어야 할까?
하나의 ref에 다 집어넣고 관리할 수는 없을까?
그리고, NavBar랑 이동할 각각의 컴포넌트들이랑 어떻게 ref를 공유할 수 있을까?

ref를 배열로 관리하면서 forwardRef()로 자식 컴포넌트에게 넘겨주기

  1. ref를 각각의 컴포넌트에 넘겨준다.
  2. 컴포넌트에서 refprops처럼 받을 수 없으므로 forwardRef()의 두 번째 인자로 넘어온 ref를 받는다.
  3. 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

profile
공부 기록

0개의 댓글