본문 바로가기

[custom hook] 스크롤을 드래그하며 페이지 이동 구현

[custom hook] 스크롤을 드래그하며 페이지 이동 구현

구현한 기능

 

스크롤 위치에 따라 시각적으로 바뀌는 스크롤바 

 

마우스를 클릭 또는 누른 채 드래그 하여 페이지를 이동하는 모습

세부 기능 : 클릭 하면 바로 이동, 왼쪽 마우스 누른 후 드래그하면 페이지 이동

, ㅋ

 

구현한 방법

 

 

ScrollProgress.tsx

'use client';

import React, { memo, RefObject } from 'react';
import { useScrollProgress } from '@/hooks/useScrollProgress';
import styles from './ScrollProgress.module.scss';

interface ScrollProgressProps {
  mainRef: RefObject<HTMLElement>; // 여기서 mainRef는 body를 감싸는 전체 컴포넌트이다.
  lng: string;
}

const ScrollProgress = memo(({ mainRef, lng }: ScrollProgressProps) => {
  const { containerRef, barRef, progressRef, handleProgressMove } = useScrollProgress({
    mainRef,
  });

  return (
  // position이 absolute인 컴포넌트 (원하는 곳에 스크롤바를 배치시키기 위함)
    <div className={styles.container} ref={containerRef}>
     // 실제 세로로 긴 스크롤바 전체를 감싸는 element  
     //  마우스를 눌렀을 때, 움직일 때, 클릭했을 때 이벤트가 걸림 
      <button
        type="button"
        className={styles.bar}
        ref={barRef}
        onClick={handleProgressMove}
        onMouseMove={handleProgressMove}
        onMouseDown={handleProgressMove}
        aria-label={lng === 'ko' ? '스크롤바' : 'scroll bar'}
      >
      // 스크롤에 따라 높이가 바뀌는 element 
        <div className={styles.progress} ref={progressRef} />
      </button>
    </div>
  );
});

export default ScrollProgress;

 

ScrollProgress.module.scss
.container {
  position: absolute;
  top: 80px;
  width: 100%;
  height: 100vh;

  .bar {
    display: flex;
    position: fixed;
    z-index: 10;
    top: 73px;
    right: 0;
    flex-direction: column;
    width: 2%;
    height: calc(100% - 73px); 

    .progress {
      width: 100%;
      background: linear-gradient(200deg, var(--color-bg), var(--color-primary));
    }
  }
}



커스텀훅

 

의사 코드

 

1) 스크롤 바 UI 표시하기

  1. mouseDown, mouseMove, onClick에 대한 이벤트 정보를 담을 useState 배열을 선언해준다.
  2. event.type이 'click'으로 들어오면 배열을 비워준다.
  3. 그 이외는 모두 배열 안에 차례대로 담아둔다.
    (ex- ['mousedown', 'mousemove', 'mousemove'...])
  4. 이벤트 정보에 'mousedown', 'mousemove', 'click'이 다 들어있거나, event.type이 'click'인 경우 아래 내용이 실행된다.
    • 첫 번째 경우는 마우스를 꾹 누르고 드래그했다가 떼는 경우  
    • 두 번째 경우는 바로 마우스를 클릭하는 경우 
  5. 마우스로 누른 곳이 전체 스크롤 바에서 몇 %의 위치에 있는지 찾고, 
    1. event 객체에서 clientY 추출 (현재 스크롤 높이)
    2. 전체 스크롤 높이 추출
    3. 퍼센테이지는 현재 스크롤 높이에서 헤더 높이를 뺀 값을 전체 스크롤 값으로 나누고 100을 곱해준다.
  6. 전체 페이지 기준 해당 %에 해당되는 스크롤로 이동시킨다.
    1. 전체 페이지 높이는 전체 body 값에서 헤더 높이, 푸터 높이 빼고, 현재 보이는 페이지인 100vh도 빼준다. 
    2. 이렇게 구해진 전체 페이지에 위에서 구한 %를 곱해준다.  

 

2) 스크롤바 드래그 또는 클릭 시 해당 스크롤 높이로 이동시키기 

  1. useEffect로 scroll 이벤트가 일어날 때 작동시킬 콜백함수(handleScroll)를 window에 걸어준다. 
  2. handleScroll의 내용은 아래와 같다.
    • window.scrollY로부터 scrollTop 값 추출
      • 만약 scrollTop 값이 0이면 스크롤바 높이 상태를 0으로 변경 
    • 실제로 스크롤이 이동한 화면 높이 값 구하기
      • 전체 body 값에서 헤더 높이, 푸터 높이 빼고, 현재 보이는 페이지인 100vh도 빼준다. 
    • 스크롤바 높이 표시를 위해 현재 높이 % 값 구하기
      • 현재 scrollTop 값 / 실제로 스크롤이 이동한 화면 높이 * 100 (%)
      • 스크롤바 높이 상태를 구한 값으로 변경
      • 변경된 스크롤바 높이 값을 css height로도 적용하여 동기화

 

 

import { MouseEvent, RefObject, useCallback, useEffect, useRef, useState } from 'react';

interface ScrollProgressProps {
  mainRef: RefObject<HTMLElement>;
}

export const useScrollProgress = ({ mainRef }: ScrollProgressProps) => {
  const [height, setHeight] = useState<number>(0);
  const containerRef = useRef<HTMLDivElement | null>(null);
  const barRef = useRef<HTMLButtonElement | null>(null);
  const progressRef = useRef<HTMLDivElement | null>(null);
  
  const [events, setEvents] = useState<string[]>([]);

  const HEADER_HEIGHT = 80;
  const FOOTER_HEIGHT = 60 + 32;

  useEffect(() => {
    window.addEventListener('scroll', handleScroll, true);

    return () => {
      window.removeEventListener('scroll', handleScroll, true);
    };
  }, [handleScroll]);

  const handleScroll = useCallback(() => {
    if (barRef.current && containerRef.current && mainRef.current) {
      const scrollTop = window.scrollY;

      if (scrollTop === 0) {
        setHeight(0);
        return;
      }

      const windowHeight: number =
        mainRef.current.offsetHeight +
        HEADER_HEIGHT +
        FOOTER_HEIGHT -
        containerRef.current.offsetHeight;
      const currentPercent: number = scrollTop / windowHeight * 100;

      setHeight(currentPercent);

      if (progressRef.current) {
        progressRef.current.style.height = `${currentPercent}%`;
      }
    }
  }, [height]);

  const handleProgressMove = useCallback(
    (event: MouseEvent<HTMLButtonElement>): void => {
      if (event.type === 'click') {
        setEvents(() => []);
      }
      setEvents((prev) => [...prev, event.type]);

      if (
        barRef.current &&
        containerRef.current &&
        mainRef.current &&
        ((events.includes('mousedown') &&
          events.includes('mousemove') &&
          events.includes('click')) ||
          event.type === 'click')
      ) {
        const { scrollHeight } = barRef.current;
        const { clientY } = event;

        const selectedPercent = ((clientY - HEADER_HEIGHT) / scrollHeight) * 100;

        const windowHeight =
          mainRef.current.offsetHeight +
          HEADER_HEIGHT +
          FOOTER_HEIGHT -
          containerRef.current.offsetHeight;

        const moveScrollPercent = (windowHeight * selectedPercent) / 100;

        window.scrollTo({
          top: moveScrollPercent,
          behavior: 'smooth',
        });
      }
    },
    [events],
  );

  return { containerRef, barRef, progressRef, handleProgressMove };
};

 

728x90
⬆︎