[React] 요소 외 외부 영역 클릭

hyejinJo·2024년 3월 24일
0

React

목록 보기
9/9
post-thumbnail

[인스타그램 클론 코딩 프로젝트] 작업중, 더보기 버튼을 작업하는 중이었다. 버튼을 토글 클릭시 메뉴창이 뜨고 닫히도록 만들어놓은 상태이다.

'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 이벤트로 isOpenedfalse 로 만드는 방법도 있었지만 메뉴창이 하나의 페이지가 아닌, 컴포넌트 영역에 있었기 때문에 해당 방법은 쓰지 못했다. 대신 useEffectuseRef 훅을 사용하여 해결하게 되었다.

메뉴창(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-외부-영역-클릭-시-닫기

profile
FE Developer 💡

0개의 댓글