구현한 기능
스크롤 위치에 따라 시각적으로 바뀌는 스크롤바
마우스를 클릭 또는 누른 채 드래그 하여 페이지를 이동하는 모습
세부 기능 : 클릭 하면 바로 이동, 왼쪽 마우스 누른 후 드래그하면 페이지 이동
구현한 방법
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 표시하기
- mouseDown, mouseMove, onClick에 대한 이벤트 정보를 담을 useState 배열을 선언해준다.
- event.type이 'click'으로 들어오면 배열을 비워준다.
- 그 이외는 모두 배열 안에 차례대로 담아둔다.
(ex- ['mousedown', 'mousemove', 'mousemove'...]) - 이벤트 정보에 'mousedown', 'mousemove', 'click'이 다 들어있거나, event.type이 'click'인 경우 아래 내용이 실행된다.
- 첫 번째 경우는 마우스를 꾹 누르고 드래그했다가 떼는 경우
- 두 번째 경우는 바로 마우스를 클릭하는 경우
- 마우스로 누른 곳이 전체 스크롤 바에서 몇 %의 위치에 있는지 찾고,
- event 객체에서 clientY 추출 (현재 스크롤 높이)
- 전체 스크롤 높이 추출
- 퍼센테이지는 현재 스크롤 높이에서 헤더 높이를 뺀 값을 전체 스크롤 값으로 나누고 100을 곱해준다.
- 전체 페이지 기준 해당 %에 해당되는 스크롤로 이동시킨다.
- 전체 페이지 높이는 전체 body 값에서 헤더 높이, 푸터 높이 빼고, 현재 보이는 페이지인 100vh도 빼준다.
- 이렇게 구해진 전체 페이지에 위에서 구한 %를 곱해준다.
2) 스크롤바 드래그 또는 클릭 시 해당 스크롤 높이로 이동시키기
- useEffect로 scroll 이벤트가 일어날 때 작동시킬 콜백함수(handleScroll)를 window에 걸어준다.
- handleScroll의 내용은 아래와 같다.
- window.scrollY로부터 scrollTop 값 추출
- 만약 scrollTop 값이 0이면 스크롤바 높이 상태를 0으로 변경
- 실제로 스크롤이 이동한 화면 높이 값 구하기
- 전체 body 값에서 헤더 높이, 푸터 높이 빼고, 현재 보이는 페이지인 100vh도 빼준다.
- 스크롤바 높이 표시를 위해 현재 높이 % 값 구하기
- 현재 scrollTop 값 / 실제로 스크롤이 이동한 화면 높이 * 100 (%)
- 스크롤바 높이 상태를 구한 값으로 변경
- 변경된 스크롤바 높이 값을 css height로도 적용하여 동기화
- window.scrollY로부터 scrollTop 값 추출
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
'📌 PROJECT > 2309 다국어 지원 포트폴리오' 카테고리의 다른 글
[KPT 회고] 버전 1.2 마무리 (2) | 2023.10.29 |
---|---|
[custom hook] 페이지 최상단으로 이동하는 버튼 (0) | 2023.10.29 |
[Next.js 13] SSR로 동적 meta태그 생성하기 (app router) (0) | 2023.10.20 |
pnpm을 이용한 모노레포 마이그레이션 (0) | 2023.10.18 |
NotionAPI로 블로그 만들기 (3) Next.js App Router 미들웨어로 redirection 설정하기 (0) | 2023.10.15 |