본문 바로가기
🔥Next 뽀개기

드롭다운 메뉴 직접 구현하기

by 짱돌보리 2025. 4. 4.
728x90

Next.js에서 드롭다운 메뉴 구현하기🔼🔽

1. Dropdown 컴포넌트 구현

사용자가 드롭다운을 클릭하면 메뉴를 열고, 옵션을 선택할 수 있게 해보자!

 

isOpen 상태로 드롭다운의 열림/닫힘을 관리하고, selectedOption으로 선택된 옵션을 관리한다.

외부 클릭 시 드롭다운 닫힘

  • useRef는 React에서 DOM 요소에 접근할 수 있게 해주는 훅이다. 드롭다운 컴포넌트의 루트 div에 ref를 연결하고, 클릭이 드롭다운 바깥에서 일어났는지 확인할 수 있다!
  • useEffect를 통해 외부 클릭을 감지할 수 있다. mousedown 이벤트를 사용해 마우스로 클릭이 일어났을 때, 클릭이 드롭다운 메뉴 내부인지 외부인지를 감지하고, 외부라면 드롭다운을 닫는다!
type DropdownProps = {
  onSelect: (option: string) => void
}

export default function Dropdown({ onSelect }: DropdownProps) {
  const [isOpen, setIsOpen] = useState(false) // 드롭다운이 열려 있는지 상태 관리
  const [selectedOption, setSelectedOption] = useState('최신순') // 기본 선택 옵션

  const dropdownRef = useRef<HTMLDivElement>(null) // 드롭다운 외부 클릭 감지를 위한 ref

  const options = ['최신순', '좋아요순', '조회순']

  const toggleDropdown = () => {
    setIsOpen((prev) => !prev) // 드롭다운 열기/닫기
  }

  const selectOption = (option: string) => {
    setSelectedOption(option) // 옵션 선택
    onSelect(option) // 부모 컴포넌트에 선택된 옵션 전달
    setIsOpen(false) // 드롭다운 닫기
  }

  // 외부 클릭 시 드롭다운 닫기
  useEffect(() => {
    const handleClickOutside = (e: MouseEvent) => {
      if (
        dropdownRef.current &&
        !dropdownRef.current.contains(e.target as Node)
      ) {
        setIsOpen(false)
      }
    }

    document.addEventListener('mousedown', handleClickOutside)

    return () => {
      document.removeEventListener('mousedown', handleClickOutside)
    }
  }, [])

  return (
    <div
      ref={dropdownRef}
      className="bg-custom-gray-300 border-custom-gray-200 text-custom-white relative flex cursor-pointer rounded-lg border px-4 py-2"
    >
      <div
        onClick={toggleDropdown}
        className="flex w-full cursor-pointer items-center justify-between"
      >
        <span className="text-white">{selectedOption}</span>
        <span className="ml-2">{isOpen ? '🔼' : '🔽'}</span>
      </div>

      {isOpen && (
        <div className="bg-custom-gray-300 border-custom-gray-200 absolute left-0 mt-10 w-full rounded-lg border shadow-md">
          {options.map((option, index) => (
            <div
              key={index}
              onClick={() => selectOption(option)}
              className="cursor-pointer rounded-lg px-4 py-2 hover:bg-gray-950"
            >
              {option}
            </div>
          ))}
        </div>
      )}
    </div>
  )
}

 

GPT가 만들어준 이미지 ㅋ 좋구만

 

  • dropdownRef.current
    👉 드롭다운 DOM 요소가 실제로 존재하는지 확인
  • !dropdownRef.current.contains(e.target as Node)
    👉 클릭한 요소(e.target)가 드롭다운 안에 속하지 않은 경우

-> "드롭다운이 있고, 사용자가 드롭다운 바깥을 클릭했다면" setIsOpen(false) 해서 드롭다운을 닫는다

 

⚠️ 드롭다운 메뉴 자동 닫힘 오류

메뉴에서 옵션을 선택하면 드롭다운이 닫혀야 하는데, 메뉴가 계속 열려 있었다. 왜냐하면, 드롭다운 아이템을 클릭할 때마다 드롭다운을 열려고 하는 toggleDropdown 함수가 실행돼서 메뉴가 닫히지 않는 것이었돠!

 

기존코드))

<div
  ref={dropdownRef}
  onClick={toggleDropdown}
  className="bg-custom-gray-300 border-custom-gray-200 text-custom-white relative flex cursor-pointer rounded-lg border px-4 py-2"
>
  <div
    onClick={toggleDropdown}
    className="flex w-full cursor-pointer items-center justify-between"
  >
    <span className="text-white">{selectedOption}</span>
    <span className="ml-2">{isOpen ? '🔼' : '🔽'}</span>
  </div>

  {isOpen && (
    <div className="bg-custom-gray-300 border-custom-gray-200 absolute left-0 mt-10 w-full rounded-lg border shadow-md">
      {options.map((option, index) => (
        <div
          key={index}
          onClick={() => selectOption(option)}
          className="cursor-pointer rounded-lg px-4 py-2 hover:bg-gray-950"
        >
          {option}
        </div>
      ))}
    </div>
  )}
</div>

 

변경 후))

<div
  ref={dropdownRef}
  className="bg-custom-gray-300 border-custom-gray-200 text-custom-white relative flex cursor-pointer rounded-lg border px-4 py-2"
>
  <div
    onClick={toggleDropdown}
    className="flex w-full cursor-pointer items-center justify-between"
  >
    <span className="text-white">{selectedOption}</span>
    <span className="ml-2">{isOpen ? '🔼' : '🔽'}</span>
  </div>

  {isOpen && (
    <div className="bg-custom-gray-300 border-custom-gray-200 absolute left-0 mt-10 w-full rounded-lg border shadow-md">
      {options.map((option, index) => (
        <div
          key={index}
          onClick={() => selectOption(option)}
          className="cursor-pointer rounded-lg px-4 py-2 hover:bg-gray-950"
        >
          {option}
        </div>
      ))}
    </div>
  )}
</div>

 

드롭다운 메뉴가 열리는 버튼을 클릭할 때마다 toggleDropdown이 실행되어 드롭다운을 열고 닫히는데, 드롭다운 아이템을 선택하는 경우에도 이 함수가 실행되어 메뉴가 다시 열리려고 했음. (즉, 드롭다운 메뉴의 전체 영역에 클릭 이벤트가 걸림!!)

 

→ 드롭다운 버튼 클릭 시만 toggleDropdown 실행 ref div 안에있는 메뉴만 있는 div에 toggleDropdown을 넣어줘야한다!

 

이후 selectOption 함수를 통해 드롭다운을 닫아주면 된다.

  const selectOption = (option: string) => {
    setSelectedOption(option)
    onSelect(option)
    setIsOpen(false) // 옵션 선택 후 드롭다운 닫기
  }

2. 드롭다운 기능을 활용한 게시글 목록 정렬 구현하기

  • 기본값은 최신순
  • handleSortChange는 드롭다운에서 선택된 정렬 옵션에 따라 프로젝트 배열을 정렬하는 함수
  • 원본 projects 배열을 직접 변경하지 않기 위해 복사본을 생성후 정렬해준다!!
  const [sortedProjects, setSortedProjects] = useState<Project[]>(projects)
  const [sortOption, setSortOption] = useState('최신순')

  const handleSortChange = (option: string) => {
    setSortOption(option)
    const sorted = [...projects]

    switch (option) {
      case '최신순':
        sorted.sort(
          (a, b) =>
            new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
        )
        break
      case '좋아요순':
        sorted.sort((a, b) => b.likes - a.likes)
        break
      case '조회순':
        sorted.sort((a, b) => b.views - a.views)
        break
      default:
        break
    }

    setSortedProjects(sorted)
  }

  return (
    <div>
      <ProjectBanner />
      <div className="mx-auto w-full max-w-[1200px] px-10 py-10">
        <div className="flex items-center justify-start gap-4 py-10">
          <Dropdown onSelect={handleSortChange} />
          <SearchBar />
        </div>
        <ProjectList
          currentPage={currentPage}
          projects={sortedProjects}
          itemsPerPage={itemsPerPage}
        />
        <Pagination
          currentPage={currentPage}
          setCurrentPage={setCurrentPage}
          totalPages={totalPages}
        />
      </div>
    </div>
  )
}

 

 

구현 완!!! 🥳🥳🥳