[인스타그램 클론 코딩 프로젝트] 작업중, 더보기 버튼을 작업하는 중이었다. 버튼을 토글 클릭시 메뉴창이 뜨고 닫히도록 만들어놓은 상태이다.
'use client'
import { useState } from 'react'
import menuStyle from '@/app/(loggedIn)/_component/navMenu.module.scss'
import Link from 'next/link'
import style from './moreMenu.module.scss'
function MoreMenu() {
const [isOpened, setIsOpened] = useState<boolean>(false)
return (
<div className={style.moreMenu}>
<div className={menuStyle.menuItem}>
<button
className={menuStyle.menuButton}
type="button"
onClick={() => setIsOpened(!isOpened)}
>
{/* svg 아이콘 있음 */}
<span style={{ fontWeight: `${isOpened ? 'bold' : ''}` }}>
더보기
</span>
</button>
{isOpened && (
<ul role="dialog" className={style.moreMenuList}>
<li>
<Link href="#" className={style.moreMenuItem}>
<svg>
...
</svg>
<span>설정</span>
</Link>
</li>
<li>
<Link href="#" className={style.moreMenuItem}>
{/* svg 아이콘 있음 */}
<span>저장됨</span>
</Link>
</li>
...
</ul>
)}
</div>
</div>
)
}
export default MoreMenu
여기서 나는 메뉴창 외의 영역에도 클릭할 시 메뉴창이 닫히도록 하고싶어했다. 찾던 내용중, 단순하게 전체영역을 태그로 감싼 후 해당 전역 태그의 onClick 이벤트로 isOpened
를 false
로 만드는 방법도 있었지만 메뉴창이 하나의 페이지가 아닌, 컴포넌트 영역에 있었기 때문에 해당 방법은 쓰지 못했다. 대신 useEffect
와 useRef
훅을 사용하여 해결하게 되었다.
메뉴창(ul) 요소를 참조할 수 있겠금 dialogRef 라는 이름으로 ref
속성을 넣어준다. 그리고 useEffect
를 통해 클릭이벤트를 감지하는 함수를 넣고, 그에 대한 콜백함수는 dialogRef 요소 외의 부분이 클릭됨에 따라 isOpened
의 값이 false
로 바뀌도록 해준다.
이때 e 매개변수의 타입은 MouseEvent
로 주고, contains
의 경우 Node
인터페이스를 사용하기 때문에 e.target
의 타입을 Node
로 선언해준다.
function MoreMenu() {
const [isOpened, setIsOpened] = useState<boolean>(false)
const dialogRef = useRef<HTMLUListElement | null>(null)
useEffect(() => {
// MouseEvent 타입 매개변수를 받고, 아무것도 반환하지 않는 함수
const handleOutsideClose = (e: MouseEvent): void => {
// useRef current에 담긴 엘리먼트 이외의 영역을 클릭 시 메뉴창 사라짐
if (isOpened && !dialogRef.current.contains(e.target as Node))
setIsOpened(false)
}
document.addEventListener('click', handleOutsideClose)
return () => document.removeEventListener('click', handleOutsideClose)
}, [isOpened])
return (
<div className={style.moreMenu}>
<div className={menuStyle.menuItem}>
...
{isOpened && (
<ul role="dialog" ref={dialogRef} className={style.moreMenuList}>
<li>
<Link href="#" className={style.moreMenuItem}>
...
</li>
...
</ul>
)}
</div>
</div>
)
}
export default MoreMenu
또한 메모리 누수 방지를 위해 등록된 이벤트를 removeEventListener
를 통해 삭제해 주도록 유의해주어야 한다.
https://close-up.tistory.com/entry/React-컴포넌트-특정-영역-외-클릭-감지
https://babycoder05.tistory.com/entry/React-외부-영역-클릭-시-닫기