본문 바로가기
개발/React

useHasScroll.tsx : 컴포넌트의 스크롤바 유무를 boolean으로 리턴

by 안뇽! 2023. 4. 30.
반응형

useHasScroll.tsx : 컴포넌트의 스크롤바 유무를 boolean으로 리턴

회사에서 dragSlider.tsx를 만들었다.

이전에 라이브러리를 이용해 만든 dragSlider는 카드를 잡아끌었을때, 스크롤이 움직이지 않는 버그가 있었다.

버그를 해결하면서 만든 dargSlider는 카드를 잡아 끌면 스크롤도 같이 움직이도록 되어 있다.

 

한편, 슬라이더가 슬라이더임을 눈치 채지 못하고, 화면이 짤린것이 아니냐고 물어보는 VOC가 종종 있다는 제보를 받았다.

PM분이 '넘기는 것을 암시하는 흐린 그라데이션'을 넣는것을 제안해주셨고 나와 디자이너분 모두 찬성했다.

 

그래서 다음과 같이 오른쪽 끝에 흐린 그라데이션(이하 cloudyArea)을 넣는 UI가 만들어졌다.

오른쪽 끝에 흐려짐.

 

화면이 충분히 넓어서 slider의 컨텐츠를 다 보여줄 수 있을때는 스크롤이 필요없다. 예를들면 카드가 3개인 경우 데스크탑에서는 화면너비가 충분하여 스크롤이 필요 없다.

 

cloudyArea는 넘기는 것을 암시하기 위해 만들어졌기 때문에, 스크롤이 없을때는 cloudyArea가 필요없다.

 

문제는 이 때에도 cloudyArea가 지워지지 않고 쓸데없이 컨텐츠를 가리고 있었다.

카드가 3개인 경우 데스크탑에서는 화면너비가 충분하여 스크롤이 필요 없는데도 cloudyArea가 적용되어 컨텐츠를 일부 가리고 있다.

hasScroll 만들기

이를 해결하기 위해, 스크롤 유무에 따라 cloudyArea를 렌더링하기로 했다. 

 

다음 코드를 목표로 하고 hasScroll을 계획했다.

{hasScroll && <CloudyArea cloudyAreaBgColor={cloudyAreaBgColor} />}

이를 만들기 위해서는 scrollWidth, clientWidth 에 대한 기본지식이 있어야 하는데 이 글에 잘 설명되어 있다.

ScrollWidth

scrollWidth는 화면너머로 넘어간 영역까지 모두 포함한 스크롤 할 수 있는 전체 영역이다.

The Element.scrollWidth read-only property is a measurement of the width of an element's content, including content not visible on the screen due to overflow.

scrollWidth는 overflow때문에 screen에서 보이지 않는 컨텐트까지 포함하는 element content의 width이다.
(출처 : https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollWidth)

ClientWidth

clientWidth는 패딩을 포함한 width이다. border, margin, 그 너머(수직스크롤바)는 포함하지 않는다.

The Element.clientWidth property is zero for inline elements and elements with no CSS; otherwise, it's the inner width of an element in pixels. It includes padding but excludes borders, margins, and vertical scrollbars (if present).

element의 width인데 padding은 포함하지만 border,margin,세로스크롤바는 포함하지 않는다.
(출처 : https://developer.mozilla.org/en-US/docs/Web/API/Element/clientWidth)

clientWidth와 clientHeight를 설명한 그림

 

스크롤바 유 무 판단

overflow:auto 라고 가정했을때 스크롤바는 안보이는 영역을 포함한 컨텐츠의 영역 > 화면에 나타난 element의 padding을 포함한 너비 일 때 생긴다.  즉, scrollWidth > clientWidth 일 때 생긴다.

clientWidth와 clientHeight를 설명한 그림2
scrollWidth > clientWidth 일 때 스크롤바가 필요하다.

이해하고 다시 보면 네이밍도 직관적이다. 스크롤 너비 > 클라이언트 너비 일 때 당연히 스크롤바가 필요하다.

 

그럼 hasScroll의 상태를 다음과 같이 정의할 수 있다.

setHasScroll(sliderRef.current.scrollWidth > sliderRef.current.clientWidth);

useHasScroll

그런데 이를 그대로 작성하면 화면이 1px 변화할때마다 매번 상태변화가 일어난다.

최적화를 위해 debounce 0.5s를 주었다. (debounce 모르면 이 글 읽어보면 좋을 것 같다).

 

사실 useDeferredValue를 사용하려고 했는데, 가만 생각히보니 그냥 0.5초동안 debounce를 통해 입력을 막아주는게 가장 맞는 선택인 것 같았다.

 

여튼 debounce를 사용하면 다음과 같다.

   const handleWindowResize = debounce(() => {
      if (sliderRef.current) {
        setHasScroll(sliderRef.current.scrollWidth > sliderRef.current.clientWidth);
      }
    }, 500);

이를 useEffect에 넣고 커스텀훅 useHasScroll을 만들었다.

// useHasScroll.tsx

import { useEffect, useState, RefObject } from 'react';
import { debounce } from 'lodash';

const useHasScroll = (sliderRef: RefObject<HTMLDivElement>) => {
  const [hasScroll, setHasScroll] = useState<boolean>(true);
  useEffect(() => {
    if (!sliderRef.current) {
      return;
    }

    const handleWindowResize = debounce(() => {
      if (sliderRef.current) {
        setHasScroll(sliderRef.current.scrollWidth > sliderRef.current.clientWidth);
      }
    }, 500);

    window.addEventListener('resize', handleWindowResize);

    return () => {
      window.removeEventListener('resize', handleWindowResize);
    };
  }, []);

  return hasScroll;
};

export default useHasScroll;

사용한 코드

첫 렌더링시에는 스크롤바 유무에 업데이트 되지 않고, 초기값(hasScroll = true)이 반영되는 문제

그런데 이렇게 하니, 스크롤바가 없을때는 hasScroll이 false여야 하는데, true인 초기값이 그대로 반영되었다가,

브라우저 창을 조절하면 그때부터 hasScroll이 재 조정되었다.

 

const [hasScroll, setHasScroll] = useState<boolean>(true);

데스크탑에서 렌더링 되었을때는 스크롤이 필요없음에도 hasScroll의 초기값이 true라서 cloudyArea가 적용된 모습
브라우저 크기를 조정하면 그때부터 hasScroll이 업데이트 되면서 cloudyArea.tsx의 렌더링 여부가 결정된다.

최선의 선택은 아닌것 같은데, 고민하다가 그냥 debounce함수 밖에 setHasScroll를 한번 더 작성하였다.

import { useEffect, useState, RefObject } from 'react';
import { debounce } from 'lodash';

const useHasScroll = (sliderRef: RefObject<HTMLDivElement>) => {
  const [hasScroll, setHasScroll] = useState<boolean>(true);
  useEffect(() => {
    if (!sliderRef.current) {
      return;
    }

    // 렌더링 되자마자 hasScroll 이 업데이트 되도록 12번 line 작성
    setHasScroll(sliderRef.current.scrollWidth > sliderRef.current.clientWidth);

    const handleWindowResize = debounce(() => {
      if (sliderRef.current) {
        setHasScroll(sliderRef.current.scrollWidth > sliderRef.current.clientWidth);
      }
    }, 500);

    window.addEventListener('resize', handleWindowResize);

    return () => {
      window.removeEventListener('resize', handleWindowResize);
    };
  }, []);

  return hasScroll;
};

export default useHasScroll;

 

어쨌든 위와 같이 작성하니 의도대로 스크롤바 유무에 따라 cloudyArea.tsx 렌더링 여부를 결정할 수 있었다

사용법

const hasScroll = useHasScroll(sliderRef)

 return (
    <Container {...rest}>
      {hasCloudyArea && hasScroll && <CloudyArea cloudyAreaBgColor={cloudyAreaBgColor} />}
      <Slider
        ref={sliderRef}
        ...
        >
        {childern}
      </Slider>
    </Container>
)

 

반응형