React
BottomSheet
bkdragon
2024. 10. 7. 17:00
화면 하단에서 올라오는 BottomSheet 를 React로 구현해보려고 한다. 데스크탑 해상도에서 Modal 창을 사용하는 화면을 대체할 수 있을 것 같다.
주 기능은 아래와 같다.
- 특정 동작(버튼 클릭 등) 이후 화면 아래에서 올라온다.
- 드래그를 통해 화면 아래로 내릴 수 있다.
style은 tailwindcss를 사용했다.
주 기능을 토대로 Props의 interface 부터 만들어보자.
interface IProps {
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}
보여져야 하는 상황인지 알 수 있는 상태와 다시 안보이게 할 수 있는 함수를 받는다. UI와 기본 기능만 재활용할 것이기 때문에 내부 요소들은 children 으로 전달 받게 했다.
보이는 화면 밖에 있다가 나타나게 하는것은 position과 bottom 속성을 이용할 것이다.
fixed 에 isOpen 일 때 bottom 0, 아닐 때 (-100%, -높이)
jsx 부분 이다.
<>
<div
className={clsx(
"transition-all duration-300 left-0 right-0 top-0 bottom-0 p-4 bg-black/40 border-t-2 border-gray-200 z-25",
isOpen ? "fixed" : "hidden"
)}
/>
<div
ref={ref}
style={{
transform: `translateY(${-diffY}px)`,
height: `${defaultHeight}px`,
bottom: isOpen ? "0" : `-${height || defaultHeight}px`,
}}
className={`fixed transition-all duration-300 left-0 right-0 p-4 bg-white border-t-2 border-gray-200 rounded-tl-xl rounded-tr-xl z-50 flex flex-col`}
>
<button onTouchStart={handleTouchStart} className="flex items-center justify-center w-full">
<RiDraggable className="text-2xl rotate-90" />
</button>
<div className="flex-1 overflow-y-auto basis-0">{children}</div>
</div>
</>
첫번째 Div는 뒷 배경을 어둡게하는 역할이다. (clsx 는 조건부 스타일을 쉽게 적용하는 라이브러리, tailwind 와 조합이 좋다고 생각한다.)
높이는 화면 높이의 70%를 기본으로 가지게 했다. defaultHeight는 resize 이벤트 핸들러를 등록해서 계산한다.
const [defaultHeight, setDefaultHeight] = useState(0);
useEffect(() => {
const calculateHeight = () => {
setDefaultHeight(window?.innerHeight * 0.7 || 400);
};
calculateHeight();
window.addEventListener("resize", calculateHeight);
return () => {
window.removeEventListener("resize", calculateHeight);
};
}, []);
2번 기능은 TouchStart 이벤트를 사용해서 구현할 수 있다. TouchStart 했을 때 TouchMove 이벤트 핸들러를 등록해서 첫 터치 지점과 움직이는 지점으로 차이로 이동거리를 계산하고 이동 거리가 BottomSheet의 70퍼센트를 넘어가면 close 되게 한다.
const handleTouchStart = (e: React.TouchEvent<HTMLButtonElement>) => {
if (!ref.current) return;
const bottomBarHeight = ref.current.offsetHeight;
let startY = e.touches[0].clientY; // 여러 손가락으로 터치를 할 수 있어서 배열이다.
let diffY = 0;
const handleTouchMove = (e: TouchEvent) => {
const currentY = e.touches[0].clientY;
diffY = startY - currentY;
// 아래로만 이동하는데 70퍼 이상 이동하면 닫고, 아니면 다시 원상복구
if (diffY < 0) {
setDiffY(diffY);
if (Math.abs(diffY) > bottomBarHeight * 0.7) {
onClose();
removeEventListeners();
setDiffY(0);
}
}
};
const handleTouchEnd = () => {
setDiffY(0);
removeEventListeners();
};
const removeEventListeners = () => {
document.removeEventListener("touchmove", handleTouchMove as EventListener);
document.removeEventListener("touchend", handleTouchEnd);
};
document.addEventListener("touchmove", handleTouchMove as EventListener);
document.addEventListener("touchend", handleTouchEnd);
};
완성 화면!