[42byte] 모달 영역 바깥쪽 클릭시 모달 닫기

🤯 문제

프로젝트를 오픈하고, 느낀점이 있다면 우리는 상당히 많은 부분을 신경쓰고 구현했다고 생각했으나... 큼큼 생각보다 굉장히 예기치 못한 곳에서 미처 신경쓰지 못했던 실수들이 발견된다는 것이다.
예들들면,

...!!! 🥲


댓글안의 더보기를 컴포넌트로 분리시키고, 더보기의 펼침 유무를 상태로 각각의 댓글이 관리하다 보니 위와 같은 문제가 발생했다. 다른 댓글의 더보기를 클릭해도 이전에 클릭했던 더보기를 비활성화시킬 수 없다!

🤔 방법 찾기

더보기 아이콘을 누른 상태(true)로 신고/삭제 모달이 열리고, 그 상태에서 바깥쪽을 누르면 비활성화가 되어 모달이 사라져야 한다. 한마디로 상태가 다시 false로 바뀌어야 한다.

구글링을 해보았을 때, 모달 영역 밖을 클릭시 모달 닫기를 위한 방법은 2가지 정도가 있다.
1. 바깥쪽 영역 전체를 감싸준 뒤(ModalBackdrop), 모달이 활성화되어 있을 때 바깥쪽 클릭시 클릭 이벤트로 모달 비활성화시키기.

export default function DropdownMenu() {
  const [openDropdown, setOpenDropdown] = useState<boolean>(false);
  
  const openDropdownHandler = () => {
      setOpenDropdown(false);
  }
  
  return (
    <>
      <DropDownWrap>
        {openDropdow && (
         <ModalBackdrop onClick={openDropdownHandler}>
           <MenuList>
             <div>수정</div>
             <div>삭제</div>
           </MenuList>
         </ModalBackdrop>
         )}
      </DropDownWrap>
    </>
  );
}
  1. useRef를 이용해서 영역 바깥쪽(ModalBackdrop)을 선택하고, 모달이 활성화되어 있을 때 클릭 이벤트 타겟이 바깥쪽 ref라면 모달 비활성화 시키기.
export default function DropdownMenu() {
  const [openDropdown, setOpenDropdown] = useState<boolean>(false);
  const outSection = useRef<HTMLDivElement>(null);
  
  const openDropdownHandler = (event: React.MouseEvent<HTMLDivElement>) => {
    if (outSection.current === event.target)
      setOpenDropdown(false);
  }
  
  return (
    <>
      <DropDownWrap>
        {openDropdow && (
         <ModalBackdrop ref={outSection} onClick={openDropdownHandler}>
           <MenuList>
             <div>수정</div>
             <div>삭제</div>
           </MenuList>
         </ModalBackdrop>
         )}
      </DropDownWrap>
    </>
  );
}

우리 프로젝트는 위의 2가지 방법을 쓰지 못했는데 왜냐하면 잘 계산해서 더보기 아이콘에 position: absolute로 달아놨기 때문에 바깥쪽 영역을 묶을 수 없다.
그렇게 하게 된다면...

앱솔루트로 드롭다운이 오른쪽 끝에 가서 붙게 되고, 그럼 얼만큼 떨어져 있는지 정확하게 계산해야 하는데, 우리는 반응형으로 만들었지 때문에 그 계산이 너무 어려웠고, 또 잘 되리라는 보장이 없었다.

그래서 고민 고민 끝에, 그럼 저 드롭다운 모달이 열렸을 때,

"바깥쪽 클릭을 감지할 수 있으면 되지 않을까?"

바깥쪽을 클릭했을 때 열린 상태를 false로 만들어서 닫으면 되니까!
그렇게 생각해낸 것이 addEventListener이다.

🛠 해결하기

지금 내 문제는 컴포넌트 안에서 onClick으로 해결하지 못하기 때문에 지금 위치의 컴포넌트 바같을 클릭해도 클릭이벤트를 받아올 수 있어야 한다. 그래서 window.addEventListener('click', ...)을 사용했다.
컴포넌트자체에서 렌더링될 때 openDropdown이 열려있을 때 이벤트리스너를 싱행시키기 위해 useEffect로 감싸준다. 처음 렌더링 될때 무조건 실행!

export default function DropdownMenu() {
  const [openDropdown, setOpenDropdown] = useState<boolean>(false);
  
  const openDropdownHandler = () => {
      setOpenDropdown(false);
  }
  
  useEffect(() => {
    if (openDropdown) {            // 모달이 열려 있으면
      window.addEventListener(
        'click',                   // 클릭이 일어났을 때
        () => {
          setOpenDropdown(false);  // 모달을 닫는다!
        },
        { once: true },            // 한번만 실행되며 기억되지 않음 
      );
    }
  });

{ once: true }로 조건을 걸어준 이유는 window에서 위 이벤트리스너를 기억하기 때문에 저 조건을 안 걸어줬더니 그냥 실행했을 때, 최초 1번은 작동을 했지만 모달이 닫히고 다시 눌렀을 때 열리지 않는 것이다!

그리고 모달 리스트 안에는 stopPropagation을 걸어서 클릭 이벤트가 안 먹히도록 한다! 그럼 바깥쪽에만 클릭했을 때 모달이 닫힌다!😂

🛠 코드

export default function DropdownMenu() {
  const [openDropdown, setOpenDropdown] = useState<boolean>(false);
  
  const openDropdownHandler = () => {
      setOpenDropdown(false);
  }
  
  useEffect(() => {
    if (openDropdown) {        
      window.addEventListener(
        'click',        
        () => {
          setOpenDropdown(false);
        },
        { once: true },         
      );
    }
  });
  
  return (
    <>
      <DropDownWrap>
        {openDropdow && (
           <MenuList onClick={(e) => e.stopPropagation()}>
             <div>수정</div>
             <div>삭제</div>
           </MenuList>
         </ModalBackdrop>
         )}
      </DropDownWrap>
    </>
  );

이걸 드디어 해냈다... 🤯
여기저기 구글링을 해보며, 이것 저것 해보면서 참 다양한 방법이 있다는 것도 배울 수 있었다.
그래도 못 해내면 잠도 안 올 것 같아서 조금만 찾아봐야지봐야지 했는데 그 날 끝내서 참 다행이었다. 휴😭

근데, 이제 와서 블로그에 정리하며 든 생각은... 그럼 저 더보기를 누를 때마다 useEffect가 실행되면서 렌더링이 일어나는 것 아닌가?! 지금은 텍스트만 있어서 큰 부담은 없지만 혹시나 나중에 이미지나 gif를 넣을 수 있게 된다면... 🤷‍♀️ (그렇게 된다면 생각하도록 하자.)

참고
https://dkmqflx.github.io/frontend/2021/04/26/react-modal-close/
https://white-salt.tistory.com/25
https://developer.mozilla.org/ko/docs/Web/API/EventTarget/addEventListener
https://pa-pico.tistory.com/20

좋은 웹페이지 즐겨찾기