bkdragon's log

BottomSheet 본문

React

BottomSheet

bkdragon 2024. 10. 7. 17:00

화면 하단에서 올라오는 BottomSheet 를 React로 구현해보려고 한다. 데스크탑 해상도에서 Modal 창을 사용하는 화면을 대체할 수 있을 것 같다.

 

주 기능은 아래와 같다.

  1. 특정 동작(버튼 클릭 등) 이후 화면 아래에서 올라온다.
  2. 드래그를 통해 화면 아래로 내릴 수 있다.

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);
    };

 

 

완성 화면!

'React' 카테고리의 다른 글

Storybook 으로 협업하기  (1) 2024.10.30
useReducer Action 객체 타입  (1) 2024.10.08
렌더링과 커밋  (2) 2024.09.21
[React Query] 쿼리 키 관리하기  (0) 2024.07.16
드래그를 활용한 그리드 아이템 크기 조절  (0) 2024.06.29