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>
)
}
- 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>
)
}
구현 완!!! 🥳🥳🥳
'🔥Next 뽀개기' 카테고리의 다른 글
검색 기능 구현(feat. 디바운스) (0) | 2025.04.07 |
---|---|
Next.js 마크다운 미리보기 적용하기 (0) | 2025.03.30 |
Zustand로 로그인 상태 관리하기 (2) | 2025.03.16 |
Vercel Organization 우회 배포 & PR Preview 설정하기 (0) | 2025.03.11 |
Next.js App Router + PWA + Firebase로 알림 구현하기 (1) | 2025.01.26 |