
Next.js에서 드롭다운 메뉴 구현하기🔼🔽
게시글 목록을 정렬하기 위해 드롭다운 메뉴를 구현해보았다.
드롭다운을 클릭하면 메뉴가 열리고, 옵션을 선택하면 해당 기준으로 정렬되도록 만들었다.
1. Dropdown 컴포넌트 구현
먼저 드롭다운 UI와 상태를 관리하는 컴포넌트를 만든다.
- isOpen : 드롭다운 열림 / 닫힘 상태 관리
- selectedOption : 현재 선택된 옵션 관리
const [isOpen, setIsOpen] = useState(false)
const [selectedOption, setSelectedOption] = useState('최신순')
const dropdownRef = useRef<HTMLDivElement>(null)
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)
}
}, [])
외부 클릭 시 드롭다운 닫힘
드롭다운은 외부를 클릭했을 때 닫히도록 구현했다.
이를 위해 useRef와 useEffect를 사용했다.
useRef
useRef는 DOM 요소에 직접 접근할 수 있게 해주는 React 훅이다.
const dropdownRef = useRef<HTMLDivElement>(null)
드롭다운 루트 div에 ref를 연결해두면, 클릭한 요소가 드롭다운 내부인지 외부인지 확인할 수 있다.
외부 클릭 감지
if (
dropdownRef.current &&
!dropdownRef.current.contains(e.target as Node)
) {
setIsOpen(false)
}
이 조건의 의미는 다음과 같다.

- dropdownRef.current
👉 드롭다운 DOM 요소가 실제로 존재하는지 확인 - !dropdownRef.current.contains(e.target as Node)
👉 클릭한 요소(e.target)가 드롭다운 안에 속하지 않은 경우
-> "드롭다운이 있고, 사용자가 드롭다운 바깥을 클릭했다면" setIsOpen(false) 해서 드롭다운을 닫는다
⚠️ 드롭다운 메뉴 자동 닫힘 오류
처음 구현했을 때 문제가 있었다.
옵션을 클릭하면 드롭다운이 닫혀야 하는데 계속 열려 있었다.
원인은 이벤트 버블링 때문이었다.
기존 코드에서는 드롭다운 전체 영역에 toggleDropdown이 걸려 있었다.
기존코드))
<div
ref={dropdownRef}
onClick={toggleDropdown}
>
그래서 드롭다운 메뉴 아이템을 클릭할 때도
toggleDropdown이 실행되어 메뉴가 다시 열리게 되는 문제가 발생했다.
변경 후))
드롭다운을 여는 이벤트를 버튼 영역에만 적용했다.
<div ref={dropdownRef}>
<div onClick={toggleDropdown}>
→ 드롭다운 버튼 클릭 시만 toggleDropdown 실행 ref div 안에있는 메뉴만 있는 div에 toggleDropdown을 넣어줘야한다!
이후 selectOption 함수를 통해 드롭다운을 닫아주면 된다.
const selectOption = (option: string) => {
setSelectedOption(option)
onSelect(option)
setIsOpen(false) // 옵션 선택 후 드롭다운 닫기
}
풀코드))
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)
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 items-center justify-between"
>
<span>{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>
)
}
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>
)
}
여기서 중요한 점은
원본 배열을 직접 수정하지 않도록 복사본을 만들어 정렬하는 것이다!!
<Dropdown onSelect={handleSortChange} />
구현 끝!

'✨FRONTEND > 📍Next.js' 카테고리의 다른 글
| 프로필 이미지 업로더 구현하기(feat. 이미지 미리보기) (1) | 2025.05.10 |
|---|---|
| 검색 기능 구현(feat. 디바운스) (0) | 2025.04.07 |
| Zustand로 로그인 상태 관리하기 (2) | 2025.03.16 |
| Vercel Organization 우회 배포 & PR Preview 설정하기 (1) | 2025.03.11 |
| Next.js App Router + PWA + Firebase로 알림 구현하기 (1) | 2025.01.26 |