안녕하세요! 오늘은 TypeScript + React를 이용하여 Scroll Progressbar 컴포넌트를 제작해보려고 합니다. 위의 썸네일 사진이 Scroll Progressbar의 대표적인 예시 모습입니다. 저희는 스크롤이 진행될때마다 위의 사진처럼 기능이 동작하도록 만들어보겠습니다.
이번 과정을 진행하면서 수학 공식인 퍼센트 계산법에 대하여 알아야 합니다.
아래 링크를 통해서 퍼센트 계산법에 대한 설명들을 쉽게 알아볼 수 있습니다. 저도 아래 글을 보면서 기능을 구현하는데 많은 도움이 되었습니다.
퍼센트 계산법을 구하는데 도움이 되는 글
https://sosolife.tistory.com/518
저는 src 디렉토리에 components라는 폴더를 만든다음, ScrollProgress라는 컴포넌트를 생성해주었습니다. 이번 글에서는 Scss로 스타일링을 해주겠습니다. 자신의 취향대로 컴포넌트를 생성해주세요 😉
ScrollProgress.tsx 파일을 열어 저는 아래의 코드들을 추가해주었습니다.
추가적으로 나중에 기능을 구현할때 필요한 useState와 useRef 변수들을 추가 했습니다.
import React, {
useState,
useCallback,
useEffect,
useRef,
MouseEvent,
} from "react";
import "./ScrollProgress.scss";
const ScrollProgress = () => {
const [width, setWidth] = useState<number>(0);
// 스크롤 진행도에 따른 width 상태 관리
const progressRef = useRef<HTMLDivElement | null>(null);
// 가장 부모태그에 ref를 걸어주기 위한 ref 변수
return (
<div
className="ScrollProgress"
ref={progressRef}
>
<div
className="ScrollProgress-Progress"
style={{ width: width + '%' }}
>
</div>
</div>
);
};
export default ScrollProgress;
아래는 ScrollProgress.scss 스타일링 코드입니다.
.ScrollProgress {
width: 100%;
height: 4px;
background-color: gray;
position: fixed;
/* 가장 상단에 고정 배치 */
top: 0;
left: 0;
right: 0;
z-index: 10;
&-Progress {
height: 100%;
background-color: blue;
}
}
이제 컴포넌트 스타일링은 모두 끝났고, 이제 본격적인 기능들을 구현해보도록 하겠습니다.
이번에는 현재 스크롤이 내려온정도가 30% 정도 된다면, 상단에도 30%가 색깔로 채워지는, 즉 스크롤 진행도를 구현해보겠습니다. 어쩌면 Scroll Progressbar의 가장 기본적인 기능이죠. 코드를 보기전에, 아래 사진을 보면서 window scroll 객체에 대한 정보들을 알아놓는 것이 좋습니다.
ScrollProgress.tsx에 아래의 함수를 추가해주세요! 이제부터 0번째 토픽에서 미리 알려드린 퍼센트 구하기 공식이 나올겁니다.
const handleScroll = useCallback((): void => {
const {
scrollTop,
scrollHeight,
clientHeight
} = document.documentElement;
if (scrollTop === 0) {
// 스크롤바가 가장 위에있을때는 0으로 처리
setWidth(0);
return;
}
const windowHeight: number = scrollHeight - clientHeight;
// 스크롤바 크기 = (내용 전체의 높이) - (스크롤바를 제외한 클라이언트 높이)
const currentPercent: number = (scrollTop / windowHeight);
// 스크롤바 크기 기준으로 scrollTop이 내려온만큼에 따라 계산 (계산시 소수점 둘째자리까지 반환)
setWidth(currentPercent * 100);
// 소수점 둘째자리 까지이므로, 100을 곱하여 정수로 만들어줍니다.
}, []);
상태 관리중인 width 변수가 스크롤 할때마다 적용이 되도록, addEventListener에 추가해주도록 하겠습니다. 아래의 useEffect 코드를 ScrollProgress.tsx에 추가해주세요!
useEffect(() => {
window.addEventListener('scroll', handleScroll, true);
return () => {
window.removeEventListener('scroll', handleScroll, true);
}
}, [handleScroll]);
이제 height를 300vh 등으로 아주 크게 늘려서 스크롤 해본뒤, 결과를 확인해보세요!
이런식으로 스크롤을 할때마다 비율에 따른 컬러 width가 올바르게 작동하나요? 이번 기능은 퍼센트 구하기 공식을 잘 이용한다면 어렵지 않게 구현할 수 있었습니다. 😉
이번에 구현해볼 기능은 위의 사진처럼 클릭부분에 따른 비율 퍼센트로 스크롤이 이동되는 기능을 구현해보도록 하겠습니다. 앞서, 1번 토픽의 코드에서 ref를 지정해준적이 있었는데요, 이제 이 ref 변수를 사용하여 기능을 구현합니다.
아래의 함수를 ScrollProgress.tsx에 추가해주세요!
const handleProgressMove = useCallback((e: MouseEvent<HTMLDivElement>): void => {
if (progressRef.current !== null) {
const { scrollWidth } = progressRef.current;
const { clientX } = e;
const selectedPercent: number = ((clientX / scrollWidth) * 100);
// 선택한 x좌표(px)가 scrollWidth(px) 의 몇퍼센트인지 계산
setWidth(selectedPercent);
const { scrollHeight, clientHeight } = document.body;
const windowHeight: number = scrollHeight - clientHeight;
const moveScrollPercent: number = ((windowHeight * selectedPercent) / 100);
// 스크롤바 크기에서 선택한 좌표의 퍼센트가 몇(px)인지 계산
window.scrollTo({
top: moveScrollPercent,
// 해당지점으로 스크롤 이동
behavior: 'smooth',
})
}
}, []);
위의 함수를 JSX 최상단 div 태그의 onClick으로 걸어주세요!
<div
className={cx('ScrollProgress')}
ref={progressRef}
onClick={handleProgressMove}
></div>
모든 코드를 적고나서, Scroll Progressbar의 아무 부분을 클릭해보세요! 해당 부분의 비율에 따른 지점으로 올바르게 스크롤이 되는 기능을 구현하였습니다. 😀
이번에 주제로 한 Scroll Progressbar는 처음에 저의 포트폴리오 사이트에 넣기 위해서 제작 했었습니다. 그때 만들면서 생각보다 어렵지 않았고, 과정도 간단해서 그런지, 이번에 주제로 정하여 글을 작성하게 되었습니다. 글을 읽어보면서 궁금한점이 생기시면 댓글로 남겨주세요! 이상으로 글을 마치겠습니다. 긴 글 읽어주셔서 감사합니다 😊
import React, { useState, useCallback, useEffect, useRef, memo, MouseEvent } from 'react';
import classNames from 'classnames';
import { ClassNamesFn } from 'classnames/types';
const style = require('./ScrollProgress.scss');
const cx: ClassNamesFn = classNames.bind(style);
const ScrollProgress = memo((): JSX.Element => {
const [width, setWidth] = useState<number>(0);
const progressRef = useRef<HTMLDivElement | null>(null);
const handleProgressMove = useCallback((e: MouseEvent<HTMLDivElement>): void => {
if (progressRef.current !== null) {
const { scrollWidth } = progressRef.current;
const { clientX } = e;
const selectedPercent: number = ((clientX / scrollWidth) * 100);
setWidth(selectedPercent);
const { scrollHeight, clientHeight } = document.body;
const windowHeight: number = scrollHeight - clientHeight;
const moveScrollPercent: number = ((windowHeight * selectedPercent) / 100);
window.scrollTo({
top: moveScrollPercent,
behavior: 'smooth',
})
}
}, []);
const handleScroll = useCallback((): void => {
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
if (scrollTop === 0) {
setWidth(0);
return;
}
const windowHeight: number = scrollHeight - clientHeight;
const currentPercent: number = (scrollTop / windowHeight);
setWidth(currentPercent * 100);
}, []);
useEffect(() => {
window.addEventListener('scroll', handleScroll, true);
return () => {
window.removeEventListener('scroll', handleScroll, true);
}
}, [handleScroll]);
return (
<div className={cx('ScrollProgress')} ref={progressRef} onClick={handleProgressMove}>
<div className={cx('ScrollProgress-Progress')} style={{ width: width + '%' }} ></div>
</div>
);
});
export default ScrollProgress;
.ScrollProgress {
width: 100%;
height: 4px;
background-color: var(--gray);
position: fixed;
top: 0;
left: 0;
z-index: 10;
&-Progress {
height: 100%;
background-color: var(--blue);
}
}