[DevLog]div에서 onBlur 이벤트 사용하기 - 드롭다운, 셀렉트

on blur event 1

Forrest를 개발하던 중 드롭다운 메뉴를 개발하였다. 드롭다운 항목을 누르면 위 그림과 같이 옵션 리스트가 포함되는 형식의 화면이었다. 여기서 추가적으로 요청된 부분은 목록이 띄워져 있을 때, 목록 밖의 항목을 클릭하면 목록이 닫히도록 하는 요구 사항이었다.

이전에 리액트 네이티브 개발을 할 땐, 컴포넌트에 내장된 onBlur 이벤트가 있어서 쉽게 해당 부분을 처리할 수 있었지만, 어떤 방식으로 할 지 좋은 방법이 떠오르지 않았다.

기존 코드

기존의 코드는 아래와 같았다. 기존 코드에서는 다시 목록을 여는 항목을 클릭해야만 목록이 닫히는 상황이었다.

<div className="relative text-6xl">
  <div className="flex flex-row justify-center items-center">
    <p className="mr-5">
      <span className="text-primary font-bold">{mainHoliday.name}</span>까지
    </p>
    <div
      onClick={() => {
        setIsOpened(!isOpened);
      }}
    >
      <ArrowDown />
    </div>
  </div>
  <div className="flex justify-center">
    {isOpened && (
      <div className="absolute top-20 xl:left-[49.3%] lg:left-[48.5%] text-left text-xl bg-white w-80 rounded-2xl shadow-3xl border-solid px-3 py-2">
        <div className="relative w-[25px] left-[45%] bottom-4 border-b-8 border-x-[13px] border-x-transparent border-white shadow-2xl"></div>
        <ul className="overflow-y-auto h-72 ">
          {remainHolidays.map((holiday, idx) => {
            return (
              <li
                className="p-4 rounded-md hover:bg-background"
                key={holiday.name}
                onClick={e => {
                  handleClick(e);
                }}
              >
                {holiday.name}
              </li>
            );
          })}
        </ul>
      </div>
    )}
  </div>
</div>

첫 번째 시도

처음에 리액트 네이티브에서 같은 기능을 개발했었던 경험을 떠올려, divonBlur 이벤트를 걸어보려고 했지만, 도통 이벤트가 발생되지 않았다. 그래서 다소 무식(?)한 방법을 시도했다.

// ...
<div className="flex justify-center">
  {isOpened && (
    <>
      <div
        className="w-[100vw] h-[100vw] fixed left-0 top-0 bg-transparent"
        onClick={() => {
          setIsOpened(!isOpened);
        }}
      ></div>
      <div className="absolute top-20 text-left text-base xl:text-xl lg:text-xl sm:text-base md:text-base bg-white w-64 sm:w-64 md:w-80 lg:w-80 xl:w-80 rounded-2xl shadow-3xl border-solid px-3 py-2 z-50">
        <div className="relative w-[25px] left-[45%] bottom-4 border-b-8 border-x-[13px] border-x-transparent border-white shadow-2xl"></div>
        <ul className="overflow-y-auto h-auto">
          {remainHolidays.map(holiday => {
            return (
              <li
                className="p-4 rounded-md hover:bg-background"
                key={holiday.name}
                onClick={handleClick}
              >
                {holiday.name}
              </li>
            );
          })}
        </ul>
      </div>
    </>
  )}
</div>

위 코드와 같이 목록을 출력하기 전에 화면을 꽉 채우는 투명한 div를 생성하였다. 그리고 해당 divonClick이벤트를 걸어 해당 div가 클릭되면 목록을 닫도록 처리했다. 목록은 해당 투명한 div보다 위에 있다는 것을 나타내기 위해 z-50을 통해 z-index를 조정하였다. 이렇게 하면 의도된 대로 목록 외에 다른 부분에 클릭 이벤트가 일어날 때, 드롭다운 목록이 닫혔다. 사용 상에 큰 문제는 없었지만, 특정 부분 이외의 부분을 클릭하거나 다른 컴포넌트와 겹쳐진 부분을 클릭했을 때 닫혀지지 않는 현상도 조금씩은 존재했다.

어쨌든 요구 사항을 반영했지만, 뭔가 포커싱과 관련된 문제인데 포커싱으로 풀지 못하니 아쉬운 마음이 있었다.

리팩터링

해당 프로젝트가 아닌 실무에서 개발을 하다가 커스텀 셀렉트 박스 컴포넌트를 구현하는 부분을 진행하고 있었다. 해당 컴포넌트도 내가 기존에 겪었던 부분과 동일하게 셀렉트 박스에서 옵션을 표시하고, 옵션 외의 부분들을 클릭했을 때 옵션 목록이 닫히도록 하는 기능이 필요했다. 해당 부분을 개발하던 중 divonBlur 이벤트를 발생시키기 위해서는 tab-index와 함께 쓰여야한다는 사실을 알게되었다.

그래서 Forrest의 기존 부분에도 활용해봐야겠다는 생각이 들었다. 새로 리팩터링한 코드는 아래와 같다.

<div
  className="relative text-lg xl:text-6xl lg:text-5xl md:text-3xl sm:text-lg pb-3 sm:pb-3 md:pb-3 lg:pb-6 xl:pb-6"
  onBlur={handleBlur}
  tabIndex={0}
>
  <div className="flex flex-row justify-center items-center z-40">
    <p
      className="mr-5"
      onClick={() => {
        setIsOpened(!isOpened);
      }}
    >
      <span className="text-primary font-bold">{mainHoliday.name}</span>까지
    </p>
    <div
      className="w-2 xl:w-5 lg:w-4 md:w-2 sm:w-2"
      onClick={() => {
        setIsOpened(!isOpened);
      }}
    >
      <ArrowDown />
    </div>
  </div>
  <div className="flex justify-center">
    {isOpened && (
      <>
        <div className="absolute top-20 text-left text-base xl:text-xl lg:text-xl sm:text-base md:text-base bg-white w-64 sm:w-64 md:w-80 lg:w-80 xl:w-80 rounded-2xl shadow-3xl border-solid px-3 py-2 z-50">
          <div className="relative w-[25px] left-[45%] bottom-4 border-b-8 border-x-[13px] border-x-transparent border-white shadow-2xl"></div>
          <ul className="overflow-y-auto h-auto">
            {remainHolidays.map(holiday => {
              return (
                <li
                  className="p-4 rounded-md hover:bg-background"
                  key={holiday.name}
                  onClick={handleClick}
                >
                  {holiday.name}
                </li>
              );
            })}
          </ul>
        </div>
      </>
    )}
  </div>
</div>

위 코드와 같이 최상위 divonBlur 이벤트와 tabIndex를 추가해주면서 해당 컴포넌트가 포커스를 잃으면 해당 목록을 닫도록 해줬다. 여기서 주의할 점은 onBlurtabIndex를 어떤 위치에 넣는지가 중요하다. 포커스를 잃을 때 이벤트가 발생할 적합한 곳에 넣어줘야 한다.

정리

divonBlur와 같은 포커스 관련 이벤트를 트리거하기 위해서는 tab-index도 같이 사용해야 한다.


Written by@[Ykss]
고이게 두지 않고 흘려보내는 개발자가 되자.

GitHubInstagramLinkedIn