시작하며...
메뉴바나 드롭다운의 외부영역을 클릭했을 때 닫히는 걸 구현하는 게 어려운 작업은 아닌데 이걸 구현하면서 삽질이 좀 많았다🥲 그래서 외부 영역 클릭 시 닫히는 로직에 대하여 자세히 정리해보려고 한다.
처음 구현한 방식
Modal 처럼 닫히도록 구현
처음에 구현한 방식은 모달(Modal)처럼 닫히도록 구현하였다. 메뉴바 뒷배경 전체화면으로 만든 후, 그 위에 메뉴바를 만드는 방식이다.
const UserMenu = ({ isOpen, onClose, className }: UserMenuProps) => {
const menuRef = useRef<HTMLDivElement | null>(null);
const handleClickOutside = (e: MouseEvent<HTMLElement>) => {
if (menuRef.current) {
e.target === menuRef.current && onClose();
}
};
if (typeof window === 'undefined') {
return <></>;
}
return (
<AnimatePresence>
{isOpen && (
<motion.div
className={`fixed inset-0 z-10 ${className}`}
onClick={handleClickOutside}
ref={menuRef}
// 생략
>
<div className="fixed right-4 top-12 z-10 h-[131px] w-[110px] flex-col rounded-10 border border-grayscale-200 bg-white shadow-md lg:right-8 lg:top-16">
{/* 생략 */}
</div>
</motion.div>
)}
</AnimatePresence>
);
};
export default UserMenu;
결과 - 불만족
메뉴바의 뒷배경이 모달처럼 전체 영역을 차지하다 보니 프로필사진 hover 했을 때 커서가 pointer 로 되지도 않고 옆에 알림 아이콘을 클릭했을 때도 바로 열리지 않게 된다. 🥲
메뉴바처럼 구현
뒷 배경 없앰
그래서 모달처럼 뒤에 배경을 없애고 메뉴바 자체만 띄우게 만들었다. 뒷배경이 없어지니깐 프로필사진에 hover 했을 때 정상적으로 커서가 pointer 로 된다.
외부 영역 클릭시 메뉴바 닫히기
이제 메뉴바 바깥 부분을 클릭했을 때 닫히는 부분을 구현해야 한다. useRef 는 DOM에 접근할 수 있도록 하는 훅인데, 이 훅을 사용하여 현재 마우스로 클릭한 부분이 useRef 영역인 메뉴바 영역 안에 포함되지 않으면, 즉 바깥영역일 때 메뉴바가 닫히도록 하는 것이다.
useRef 감싸기
useRef 를 사용하여 메뉴바 전체를 menuRef 를 감싸고 그 하위에는 메뉴바를 열기 위한 ProfileIcon 과 메뉴바인 AuthUserMenu 를 배치한다. (꼭 안에 있어야 함!!!!!)
import { useRef } from 'react';
// 생략
const TopNavigationBar = () => {
const menuRef = useRef<HTMLDivElement | null>(null);
const { value: isMenuOpen, handleOff: menuClose, handleToggle: menuToggle } = useBoolean();
// 생략
return (
/* 생략 */
<div ref={menuRef}>
<ProfileIcon onClick={menuToggle} width={24} height={24} className="cursor-pointer" />
<AuthUserMenu isOpen={isMenuOpen} handleClose={menuClose} />
</div>
/* 생략 */
);
};
export default TopNavigationBar;
menuRef current 출력
구현하기 앞서 menuRef 에는 어떤 값이 있는지 확인해보기 위해 menuRef 의 current 값을 출력해 보았다.
console.log(menuRef.current);
처음에 화면에 렌더링 되기 직전에는 초기값인 null 이 찍힌다. 그래서 현재 클릭한 영역이 menuRef 에 속하지 않으면 그 밖에 있는 부분이므로 이때 메뉴바가 닫히면 된다.
마우스 이벤트
Mdn 문서를 보면 다양한 마우스 이벤트 타입들을 볼 수 있다.
간단하게 소개하자면 이러한 마우스 이벤트 등이 있다.
- mousedown - 마우스 버튼이 눌렸을 때 발생
- mouseup - 마우스 버튼을 눌렀다가 땔 때 발생
- mousemove - 마우스가 이동할 때마다 발생
외부 영역을 클릭했을 때 닫히는 것을 구현해야 되기 때문에 mousedown 을 선택하였다.
이벤트 리스너 등록
문서 전체에 mousedown 이벤트 리스너를 등록하고 이 리스너가 handleClickOutside 라는 함수를 호출하여 클릭 이벤트를 처리한다.
그리고 return 안에는 클린업 함수로 컴포넌트가 언마운트 되거나 useEffect 가 다시 실행될 때, 메모리 누수를 방지하기 위해 이전 이벤트 리스너를 제거한다.
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
// mousedown 이벤트 발생 시 실행되는 함수
console.log("mousedown 이벤트")
};
document.addEventListener('mousedown', handleClickOutside); // 이벤트 리스너 등록
return () => {
document.removeEventListener('mousedown', handleClickOutside); // 이벤트 리스너 제거
};
}, []);
그러면 모든 영역을 클릭하면 "mousedown 이벤트" 가 출력되는 것을 확인할 수 있다.
menuRef 외부 영역 클릭 시 닫힘
이제 메뉴바 외부 영역을 클릭했을 때 닫히는 기능을 구현할 수 있게 되었고 마우스 이벤트가 발생할 때 호출되는 함수에 아래 로직을 작성하면 된다.
- menuRef.current 가 존재 (null 인 경우 방지)
- 클릭한 대상이 menuRef 의 자식 요소가 아닌 경우 (즉, 외부영역)
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
menuClose();
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [menuClose]);
결과
외부 영역을 클릭해도 잘 닫히게 되는 메뉴바를 만들게 되었다! 만족~~~~~~~
전체 코드
(다른 내용은 다 지우고 해당 코드만 올림)
const TopNavigationBar = () => {
const menuRef = useRef<HTMLDivElement | null>(null);
const { value: isMenuOpen, handleOff: menuClose, handleToggle: menuToggle } = useBoolean();
const noticeRef = useRef<HTMLDivElement | null>(null);
const { value: isNoticeOpen, handleOff: noticeClose, handleToggle: noticeToggle } = useBoolean();
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
menuClose();
}
if (noticeRef.current && !noticeRef.current.contains(e.target as Node)) {
noticeClose();
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [menuClose, noticeClose]);
return (
<div className="flex gap-6">
<div ref={noticeRef}>
<NotifyIcon onClick={noticeToggle} className="cursor-pointer" />
<NoticeMenu handleClose={noticeClose} isOpen={isNoticeOpen} />
</div>
<div ref={menuRef}>
<ProfileIcon onClick={menuToggle} className="cursor-pointer" />
<AuthUserMenu isMobile={isMobile} isOpen={isMenuOpen} handleClose={menuClose} />
</div>
</div>
);
};
export default TopNavigationBar;
삽질한 내용
어려운 내용은 아니지만 구현하면서 삽질을 했었다. ㅋㅋㅋ
1. 메뉴바 컴포넌트 위치
위에서 언급했지만 ref 를 배치한 곳 내부에 메뉴바가 있어야 한다. 그런데 나는 열리고 닫히는 모달이나 바텀시트 같은 건 항상 맨 아래에 배치했었는데 처음에 메뉴바를 아래에 둬서 작동하지 않았다. 물론 ref 를 직접 console 로 찍어서 금방 해결했다.
2. close 함수 위치
진짜 뻘짓.. 왼쪽처럼 아이콘을 눌러 모달이 열리고 닫혀야 되는데 실수로 onClick 를 div 태그에 배치해 버렸다.. 그래서 외부 영역을 클릭해도 잘 닫히지만 메뉴바 내부를 클릭해도 닫히게 된다.
그래서 메뉴바에도 ref 를 배치하여 왼쪽과 같이 비효율적인 코드를 작성하였다... 외부 영역이면서, 메뉴바 내부가 아니면 닫히도록 😅
마치며...
어려운 내용은 아니지만 이런 삽질이 있었으며 앞으로 메뉴바나 드롭다운을 구현할 때 절대 실수할 일이 없을 거 같다! 😊
'💜 프로젝트 구현' 카테고리의 다른 글
[Next.js / Zustand] 로그인 확인 및 로그아웃 구현 (0) | 2024.07.15 |
---|---|
프로젝트 초기 세팅 (with 템플릿, eslint, prettier, commitLint, tailwind) (1) | 2024.07.10 |
[React] getFetch 커스텀 훅 hook 만들기 (0) | 2024.06.03 |
[Github Action] Organization 레포지토리 vercel 미리보기 preview 배포 (0) | 2024.05.09 |
[Github Action] 깃허브 Organization 레포지토리 vercel 자동 배포 (0) | 2024.05.07 |
FE 개발자가 되고 싶은 짱잼이
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!