thumbnail
드롭다운 메뉴(Dropdown menu) 만들기
Feb 22, 2023

리액트로 드롭다운 메뉴를 만들어보자.

기능 설계

dropdown image

드롭다운은 버튼 클릭 등의 상호작용으로 요소를 활성화하면 그 아래 하위 메뉴들이 펼쳐지는 것이다. 본문에서 구현할 기능은 크게 세 가지로 나눌 수 있다.

check list

버튼으로 메뉴 열고 닫기

외부 영역 클릭 시 메뉴 닫기

메뉴 오픈 상태에서 외부 스크롤 방지


기본 틀 구성

메뉴 폼

일단 버튼 활성화와 상관없이 메뉴를 활성화했을 때의 상태로 기본 틀을 잡아준다.

export default function DropdownMenu() { const category = ['C', 'C++', 'Javascript', 'Python', 'JAVA']; return ( <div className="container"> <label>MenuBar</label> <div className="menu-wrap"> <div className="menu-form"> <div className="title">Language</div> <button className="title">▾</button> </div> <ul className="category-list"> {category.map((category) => ( <li key={category}>{category}</li> ))} </ul> </div> </div> ); } Menu form

기능 구현

버튼 활성화

dropdown button click

버튼으로 메뉴를 열고 닫을 수 있게 hook을 만들어 보자. 버튼을 클릭할 때마다 isOpened의 상태가 반대로 변하는 onButtonClick 함수를 생성하고 상탯값과 함수를 전달한다.

const useDropdown = (initialState) => { const [isOpened, setIsOpened] = useState(initialState); // 버튼 클릭 시 메뉴 열고 닫기 const onButtonClick = () => { setIsOpened(!isOpened); }; return { isOpened, onButtonClick }; };

이를 DropdownMenu에 적용해준다. isOpened가 false일 때 메뉴는 비활성화 상태이고 활성화되면 category-list 목록이 보이게 된다. className으로 상태에 따라 다른 네임을 전달하면 css도 메뉴 활성화에 따라 다르게 구현할 수 있다.

export default function DropdownMenu() { //... const { isOpened, onButtonClick } = useDropdown(false); return ( <div className="container"> <label className={`${isOpened ? 'active' : ''}`}>MenuBar</label> <div className="menu-wrap"> <div className="menu-form" className={`${isOpened ? 'active' : ''}`}> <div className="title">Language</div> <button className="title" onClick={onButtonClick}> ▾ </button> </div> {isOpened && ( <ul className="category-list"> {category.map((category) => ( <li key={category}>{category}</li> ))} </ul> )} </div> </div> ); }

외부 클릭 시 메뉴 닫기

click outside


메뉴가 열려 있을 때 다시 버튼을 클릭하지 않고도 외부 화면을 클릭하면 메뉴 창이 닫히도록 만들 것이다. 아까 만든 useDropdown hook에서 useEffect로 해당 기능을 추가해주자.

ref가 가리키는 요소에 위치하지 않으면 메뉴 창을 닫도록 하는 handleClickOutside 함수를 생성하고 메뉴 창이 열려 있을 때만 실행되게 addEventListener를 추가한다.

const useDropdown = (initialState) => { const [isOpened, setIsOpened] = useState(initialState); const dropdownRef = useRef(null); // ... useEffect(() => { // 외부 영역 클릭 시 메뉴 닫기 const handleClickOutside = (e) => { if (dropDownRef.current && !dropDownRef.current.contains(e.target)) { setIsOpened(false); } }; if (isOpened) { document.addEventListener('mousedown', handleClickOutside); } else { document.removeEventListener('mousedown', handleClickOutside); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [isOpened]); return { isOpened, dropdownRef, onButtonClick }; };

그리고 DropdownMenu로 돌아가 ref를 메뉴의 가장 바깥 컴포넌트에 추가한다.

export default function DropdownMenu() { //... const { isOpened, dropdownRef, onButtonClick } = useDropdown(false); return ( <div className="container"> <label className={`${isOpened ? 'active' : ''}`}>MenuBar</label> <div className="menu-wrap" ref={dropdownRef}> // ... </div> </div> ); }

외부 스크롤 방지

모바일 환경에서는 메뉴를 펼쳤을 때 화면에서 차지하는 비율이 높기 때문에 메뉴 이외의 화면 스크롤이 함께 동작하면 불편할 수 있다. 그래서 메뉴가 열려 있는 동안에는 외부 스크롤을 막아두도록 하자.

document.documentElement.style로 루트 요소에 접근해 원하는 속성을 제어할 수 있다.

const useDropdown = (initialState) => { // ... useEffect(() => { //... // 메뉴 오픈 상태에서 외부 스크롤 막기 if (isOpened) { document.documentElement.style.overflow = 'hidden'; document.addEventListener('mousedown', handleClickOutside); } else { document.documentElement.style.overflow = ''; document.removeEventListener('mousedown', handleClickOutside); } return () => { document.documentElement.style.overflow = ''; document.removeEventListener('mousedown', handleClickOutside); }; }, [isOpened]); return { isOpened, dropdownRef, onButtonClick }; };

prevent scroll pc

prevent scroll mobile

모바일과 pc 모두 사용하기 편리한 드롭다운 메뉴가 완성됐다!


Table Of Contents
nxnaxx blog © 2022-2024 Powered By Gatsby.