커스텀 Select의 Options가 묻히는 문제 해결하기

Yoomin Kang·2024년 6월 23일
post-thumbnail

오늘은 커스텀 Select를 구현한다면 언젠가 한 번은 직면하게 될 문제에 대해 다뤄보고자 한다.

Options는 보통 position: absolute로 설정하여 구현하는데, 이 속성으로 인해 Options의 height가 아무리 변해도 부모의 height는 변하지 않는다. 따라서, 아래와 같이 Options가 묻히는 경우가 발생한다.

어떻게 해결할 수 있을까?

간단하다. position: absolute를 버리면 된다.

Options css를 다음과 같이 설정한다.

.Options {
  position: fixed;
}

이제 Select의 위치를 받아와서 Options에 설정해주기만 하면 된다.

const [optionsPosition, setOptionsPosition] = useState({
  top: 0,
  left: 0,
  width: 0,
});
const selectRef = useRef<HTMLDivElement | null>(null);

const updateOptionsPosition = () => {
  if (selectRef.current) {
    const rect = selectRef.current.getBoundingClientRect();
    setOptionsPosition({
      top: rect.bottom,
      left: rect.left,
      width: rect.width,
    });
  }
};

위와 같이 해주고, 적당히 ref를 설정한 뒤, select를 toggle할 때마다 updateOptionsPosition을 실행해주면 된다.

그리고, 다음과 같이 Options에 style을 입혀준다.

<div
  className={styles.Options}
  style={{
    top: `${optionsPosition.top + 4}px`,
    left: `${optionsPosition.left}px`,
    width: `${optionsPosition.width}px`,
  }}>
</div>

잘 나오는 모습을 볼 수 있다.

끝!

,,,이면 좋겠지만 그렇지 않다. 우리에겐 스크롤이라는 변수가 존재한다.

스크롤을 해도 Options의 위치가 변하지 않는 문제가 있다.

이를 해결하기 위해, Event listener를 설정해줘야 한다.

단순히 window.addEventListener를 한다고 해결되는 것은 아니고, 부모 중 스크롤 가능한 요소들을 찾아 모두 Event listener을 설정해야 한다.

useEffect(() => {
  const scrollableElements = new Set<Element>();

  const findScrollableParents = (node: Element | null) => {
    if (!node) return;

    const style = window.getComputedStyle(node);
    const overflowY = style.overflowY;

    if (overflowY === 'auto' || overflowY === 'scroll') {
      scrollableElements.add(node);
    }

    findScrollableParents(node.parentElement);
  };

  const addScrollListeners = () => {
    window.addEventListener('scroll', updateOptionsPosition);
    scrollableElements.forEach((element) => {
      element.addEventListener('scroll', updateOptionsPosition);
    });
  };

  const removeScrollListeners = () => {
    window.removeEventListener('scroll', updateOptionsPosition);
    scrollableElements.forEach((element) => {
      element.removeEventListener('scroll', updateOptionsPosition);
    });
  };

  if (isOptionOpen) {
    findScrollableParents(selectRef.current);
    addScrollListeners();
    updateOptionsPosition();
  } else {
    removeScrollListeners();
  }

  return () => removeScrollListeners();
}, [isOptionOpen]);

이제 원하는대로 잘 동작한다.

뭔가 쓸데없이 열심히 삽질한 것 같기도 하지만, 여튼 해결하긴 했다.

(더 좋은 방법이 있다면 댓글로 알려주시면 감사하겠습니다.)

진짜 끝!

profile
FE Developer @Toss | GSHS 36 | Korea Univ 21

0개의 댓글