치춘짱베리굿나이스
useClickOutside 직접 구현하기 본문
useClickOutside
모달을 라이브러리를 쓰지 않고 직접 구현하면서 (react-portal
을 사용하였다) 모달 바깥을 클릭했을 때 모달이 닫히도록 구현을 하고 싶었다
유용한 훅을 많이 포함하고 있는 패키지인 react-use
를 설치하여 useClickAway
훅을 이용하면 한번에 해결되겠지만, 지금 프로젝트에서 react-use
가 설치되어 있지 않기도 했고 (이거 하나 때문에 설치하기도 애매하고) 생각보다 구현이 어렵지 않아 훅을 직접 제작해 보았다
코드
import { useEffect, useRef } from 'react';
const useClickOutside = onClicKOutside => {
const ref = useRef(null);
const handleClickOutside = (event) => {
if (ref.current && !ref.current.contains(event.target)) onClicKOutside();
};
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [ref]);
return { ref };
};
export default useClickOutside;
핸들러에 의해 실행되는 함수는 외부에서 받아올 수 있도록 하였다
이 함수는 핸들러 내부에서 조건문으로 한번 감싸지는데, 이 조건문이 어떤 역할을 하는지는 아래에 적어두었다
로직
ref으로 목적 요소 지정하기
const CharSelectModal = ({ userInfo, handleClickChar, handleClickClose }) => {
const { ref: modalRef } = useClickOutside(handleClickClose);
return (
<ModalPortal>
<div className="modal-background">
<div className="char-select-modal-container" ref={modalRef}>
...
목적 요소의 ref
attribute에 useClickOutside
내부에서 useRef
을 통해 초기화한 ref
를 넣는다
만약 모달 컴포넌트의 바깥을 눌렀을 때 핸들러를 걸고 싶다면 모달 컴포넌트의 ref
를 지정하면 된다
useEffect로 이벤트 걸기
useEffect(() => {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [ref]);
document.addEventListener
에서 document
는 최상위 요소, 나아가 페이지 (html 문서) 전체를 가리킨다
(document.querySelector()
가 페이지 내의 모든 요소들 중 하나를 선택하는 메서드 함수라는 것을 기억하자)
useEffect
를 통해 컴포넌트 마운트 시, 또는 ref
에 변화가 있을 시 페이지 전체에 이벤트 리스너를 걸어주고, 페이지의 종류가 ‘mousedown
’ 이므로 페이지 어디를 클릭해도 이벤트가 발생한다
또한, 컴포넌트 언마운트 시에는 이벤트 리스너를 제거한다
우리는 handleClickOutside
라는 이벤트 핸들러를 리스너로 페이지 전역에 걸어줄 것이다
이벤트 핸들러 선언
const handleClickOutside = (event) => {
if (ref.current && !ref.current.contains(event.target)) onClicKOutside();
};
이벤트 발생 시마다 실행될 핸들러를 선언한다
핸들러는 인자로 event (타입은 MouseEvent 이므로, 타입스크립트에서 이부분을 추가해주면 된다) 를 받는다
핸들러 내부엔 조건문이 있는데, 이 조건문은 다음과 같은 조건을 만족했을 때 발동된다
ref.current
가 존재할 때 (ref
가 특정 요소에 제대로 걸려 있을때)ref.current
가 ‘이벤트가 발생한 요소' 를 포함하고 있지 않을 때
handleClickOutside
가 발생하는 이벤트 (mousedown
) 는 컴포넌트 마운트 시 페이지 전체에 리스너가 걸려 있으므로 페이지 어디를 클릭해도 해당 핸들러가 호출된다
하지만 우리가 원하는 것은 ref
로 지정한 요소의 바깥을 클릭했을 때 이벤트가 발동하는 것이므로, ref.current
에 포함되지 않은 (자기자신이거나, 자식이 아닌) 요소를 클릭했을 경우에만 원하는 이벤트가 발동되도록 조건문으로 한번 감싸준 것이다
핸들러의 인자로 event를 받지 않아도 동작한다?!
이전 코드에서 event
에 가로줄이 쭉 그어지지만 동작은 잘 하는 것을 볼 수 있었다
event
키워드는 묵시적으로 window
객체의 event
프로퍼티를 가져오므로, 변수를 찾지 못했다는 에러 없이 제대로 동작한다
window
객체는 브라우저에서 동작하는 코드의 전역 객체 (Node.js에서의 Global
과 같다) 라고 생각하면 된다
하지만 이 방법은 deprecated 되었으며, 아예 동작을 안 하는 것은 아니지만 공식에서 더이상 권장하는 방법이 아니므로 인자를 제대로 받아 사용하자
결론
이 useClickOutside
는 결국 페이지 전역에 이벤트 리스너를 걸고, 이벤트가 발생할 때마다 발생한 위치가 어느 요소인지 판단하여 ref
로 지정한 요소 또는 그 하위 요소가 아닐 때에만 이벤트가 발생하도록 해 준다
react-use를 쓰기엔 내부에서 사용하는 훅이 별로 없어서 패키지를 설치하기엔 애매할 때 직접 구현하는 것도 좋아보이는군
참고자료
'ClientSide > React' 카테고리의 다른 글
React를 클론코딩 #1 가상 돔 (0) | 2022.10.05 |
---|---|
React (0) | 2022.10.01 |
react-portal 사용해보기 (0) | 2022.05.15 |
데이터 불러오기, Suspense (0) | 2022.05.13 |
IntersectionObserver + 무한스크롤 (0) | 2022.05.12 |