프로젝트에서 실시간 채팅을 구현하면서 기본 스크롤바가 전반적인 UI에 너무 안 어울려서 수정해야될 필요성을 느꼈다. 또한, 기본 스크롤바에 조사해보니 아래와 같은 단점들이 있었다.
기본 스크롤바가 적용된 UI
- 전체 레이아웃이 왼쪽으로 밀리는 현상이 발생함
- 전반적인 UI의 어울리지 않는 스크롤바 사용으로 UI 일관성을 저해시킴
- 스크롤하지 않는 상태에서 스크롤바가 보여 비교적 주요 정보에 대한 집중력을 떨어트림
그리고 단순히 CSS만 바꾸는 게 아니라 카카오톡을 참고해서 다양한 기능을 넣을 수 있도록 구현해야겠다는 생각이 들어서 자료조사를 시작했다
React TypeScript TailwindCSS Framer motiondisplay : none을 하면 스크롤바가 없어진다/* CustomScrollbar.css */
.scrollbar-custom {
overflow-y: auto;
}
.scrollbar-custom::-webkit-scrollbar {
display: none;
}
const handleScroll = () => {
if (containerRef.current) {
{/* 스크롤바 크기 조정 */}
// 스크롤될 컨텐츠 요소 높이 계산
// 무한 스크롤링이 될 수 있기 때문에 스크롤할 때마다 연산 필요
const contentHeight = containerRef.current.children[0].clientHeight;
const scrollbarHeightCalc =
(containerHeight / contentHeight) * containerHeight;
setScrollbarHeight(scrollbarHeightCalc);
{/* 스크롤바 위치 조정 */}
// 스크롤바가 요소의 끝을 넘지 않도록 제한
const maxScrollTop = containerHeight - scrollbarHeightCalc;
const newScrollTop =
scrollYProgress.get() * (containerHeight - scrollbarHeightCalc);
setScrollPercentage(Math.min(newScrollTop, maxScrollTop));
{/* 스크롤을 할 때만 스크롤바가 보임 */}
if (hideScrollbar) {
setShowScrollbar(true)
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
setShowScrollbar(false);
}, 1000);
}
}
};
// CustomScrollbar.tsx
import { useState, useEffect, useRef } from 'react';
import { useScroll } from 'framer-motion';
import { cn } from '@/utils/cn';
import '@/styles/CustomScrollbar.css';
type CustomScrollbarProps = {
children: React.ReactNode;
containerClassName?: string;
scrollbarClassName?: string;
hideScrollbar?: boolean;
};
const CustomScrollbar = ({
children,
containerClassName = '',
scrollbarClassName = '',
hideScrollbar = true,
}: CustomScrollbarProps) => {
const [showScrollbar, setShowScrollbar] = useState(
hideScrollbar ? false : true,
);
const [scrollPercentage, setScrollPercentage] = useState(0);
const [scrollbarHeight, setScrollbarHeight] = useState(0);
const [containerHeight, setContainerHeight] = useState<number>(0);
const containerRef = useRef<HTMLDivElement>(null);
const timeoutRef = useRef<NodeJS.Timeout>();
const { scrollYProgress } = useScroll({ container: containerRef });
useEffect(() => {
// 사용자가 보고 있는 요소 높이 계산
// 고정된 크기이기 때문에 렌더링 때 한 번만 연산
if (containerRef.current) {
setContainerHeight(containerRef.current.clientHeight);
}
}, [containerRef]);
const handleScroll = () => {
if (containerRef.current) {
{/* 스크롤바 크기 조정 */}
// 스크롤될 컨텐츠 요소 높이 계산
// 무한 스크롤링이 구현할 수 있기 때문에 스크롤할 때마다 연산 필요
const contentHeight = containerRef.current.children[0].clientHeight;
const scrollbarHeightCalc =
(containerHeight / contentHeight) * containerHeight;
setScrollbarHeight(scrollbarHeightCalc);
{/* 스크롤바 위치 조정 */}
// 스크롤바가 요소의 끝을 넘지 않도록 제한
const maxScrollTop = containerHeight - scrollbarHeightCalc;
const newScrollTop =
scrollYProgress.get() * (containerHeight - scrollbarHeightCalc);
setScrollPercentage(Math.min(newScrollTop, maxScrollTop));
{/* 스크롤을 할 때만 스크롤바가 보임 */}
if (hideScrollbar) {
setShowScrollbar(true)
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
setShowScrollbar(false);
}, 1000);
}
}
};
useEffect(() => {
// 타이머 클린업 함수
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
return (
<div className={cn('relative', containerClassName)}>
<div
ref={containerRef}
onScroll={handleScroll}
className="scrollbar-custom h-full"
>
{children}
</div>
<div
className={cn(
'absolute right-0 top-0 h-full w-3 bg-transparent transition-opacity duration-300',
showScrollbar ? 'opacity-100' : 'opacity-0',
)}
>
<div
className={cn(
'absolute w-full rounded-full bg-kt-gray-2',
scrollbarClassName,
)}
style={{
top: `${scrollPercentage}px`,
height: containerRef.current ? `${scrollbarHeight}px` : '20%',
}}
/>
</div>
</div>
);
};
export default CustomScrollbar;

이제 채팅창 무한 스크롤을 구현하면서 성능에도 더 신경써서 미흡한 부분은 고쳐봐야겠다