[React] 드롭다운 메뉴 만들기 (feat. useRef, useState)

2022. 2. 27. 15:48React

728x90

오늘은 커스텀 훅을 사용해 드롭다운 메뉴를 만들어 보도록 하겠다. 

드롭다운 메뉴의 종류가 여러 개 있지만 이 포스트에서는

그냥 평범하게 어떤 버튼을 클릭했을 때 아래와 같이 메뉴/옵션이 나오는 형식,

그리고 메뉴 밖을 클릭했을땐 메뉴가 닫히도록 설계를 해보겠다.

 

 

현재 진행중인 프로젝트에 적용된 모습

 

원리는 간단하다.

useDetectClose 라는 커스텀 훅을 만들고 현재 상태가 열린 (isOpen) 상태라면 그에 맞게 css만 적용해주면 된다.

 

그럼 커스텀 훅 부터 살펴보자.

 

hooks/useDetectCLose.js

import { useEffect, useState } from "react";

const useDetectClose = (elem, initialState) => {
  const [isOpen, setIsOpen] = useState(initialState);

  useEffect(() => {
    const onClick = (e) => {
      if (elem.current !== null && !elem.current.contains(e.target)) {
        setIsOpen(!isOpen);
      }
    };

    if (isOpen) {
      window.addEventListener("click", onClick);
    }

    return () => {
      window.removeEventListener("click", onClick);
    };
  }, [isOpen, elem]);
  return [isOpen, setIsOpen];
};

export default useDetectClose;

 

일단 버튼을 클릭했을때 나오는 드롭다운 메뉴 요소를 특정할 수 있도록 ref값과 초기값을 인자로 받게 설정을 해줬다.

그리고 만약 열린 상태라면, 전역 객체에 onClick 이벤트 핸들러를 달아주어 사용자가 클릭한 요소가 메뉴 안 요소인지 확인 후,

맞다면 닫기 상태로 변경하는 로직이다. 

 

메모리 누수 방지를 위해 mount 될 때 이벤트 핸들러를 제거해주는 것도 잊지 말자.

 

이제 hook을 DropDown 컴포넌트에 적용해주기만 하면 끝난다. 

 

components/DropDown.js

import {useRef, useState} from "react";
import Link from "react-router-dom";
import styles from "styles/components/DropDown.module.scss";
import classNames from "classNames";
import useDetectClose from "hooks/useDetectClose";


const DropDown = () => {
  const dropDownRef = useRef(null);
  const [isOpen, setIsOpen] = useDetectClose(dropDownRef, false);
  return (
    <div className={styles.dropDownMenu}>
      <button
        onClick={() => setIsOpen(!isOpen)}
      >
        메뉴 보기
      </button>

      <ul
        ref={dropDownRef}
        className={classNames(styles.menu, { [styles.active]: isOpen })}
      >
        <li>
          <Link href="/mypage">마이페이지</Link>
        </li>
       	{/* 메뉴 리스트들 */}
      </ul>
    </div>
  );
};

export default DropDown;

 

필자는 css module 방식을 사용했는데, 이건 원하는 방식으로 교체해서 사용하면 된다. 만약 열린 상태(isOpen) 일 때는 active 클래스를 달아줬다.

 

아래 스타일링은 참고만 하길 바란다.


.menu {
    background: #fff;
    border-radius: 8px;
    position: absolute;
    top: 30px;
    right: 0;
    width: 150px;
    text-align: center;
    box-shadow: 0 1px 8px rgba(0, 0, 0, 0.3);
    opacity: 0;
    visibility: hidden;
    transform: translateY(-20px);
    transition: opacity 0.4s ease, transform 0.4s ease, visibility 0.4s;
    padding: 10px;
  }
  
  
.active {
    opacity: 1;
    visibility: visible;
    transform: translateY(0);
  }