모달안에 있는 닫기 버튼을 누르지 않아도 모달 바깥의 부분을 클릭 했을 때 모달이 닫히는 기능은 사용자 경험상 꼭 필요한 기능이다.
나도 꽤 예전 프로젝트부터 이 기능을 구현 했었는데 사실 아주 간단하게 오직 useState만 가지고 구현했었다. 아주 단순하게 생각해서 모달바깥부분에 투명한 배경을 뷰포트 크기로 깔고 이곳을 클릭했을때 모달을 닫아준다. 여기서 중요한점은 모달 바깥부분을 책임질 배경레이어는 모달과 형제여야 하고 z-index로 모달을 배경보다 위로 올려준다.
즉 모달과 배경(모달 밖 영역)은 다른 층에 존재하는 것이다.(이벤트버블링을 막기위함)
다음은 그동안 구현했던 코드이다.
//모달을 사용하는 컴포넌트
const ReleaseSide = () => {
const [releaseModal,setReleaseModal] = useState(false)
const releasePopup = () => {
setReleaseModal(true)
document.body.style.overflow = "hidden";
}
const closeReleasePopup = () => {
setReleaseModal(false)
document.body.style.overflow = "unset"
}
return (
<ReleaseBtn onClick={()=>releasePopup()}>모달여는버튼</ReleaseBtn>
{releaseModal &&
<ReleaseModal closeReleasePopup={closeReleasePopup}/>})
}
/// 모달컴포넌트
interface propsType {
closeReleasePopup:() => void;
}
const ReleaseModal = ({closeReleasePopup}:propsType) => {
return (
<>
<Layer onClick={()=>closeReleasePopup()}></Layer> ///모달 배경
<ModalLayer>
모달창
</ModalLayer>
</>
///모달밖영역과 모달은 형제여야한다 모달밖영역이 모달을 감싼다면 밖이 아니라 모달을 클릭해도 모달이 닫힌다.<이벤트버블링>
);
}
//스타일컴포넌트
const Layer = styled.div`
z-index: 1500;
display: block;
background: rgba(0,0,0,0.3);
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
`
const ModalLayer = styled.div`
z-index:2000;
width: 400px;
height: 600px;
position:fixed;
top:50%;
left:50%;
transform: translate(-50%, -50%) !important;
`
그동안 프로젝트에서 늘 이렇게 단순한 코드를 이용했었다. useState만 사용하면 되는 간단한 방식이였지만 크림 클론코딩을 하며 문득 이게 옳은 방법인지 궁금했고 구글링 결과 뜻밖의 방법이 있어 적용해보았다.
이전의 방법보다 이 방법을 사용하며 자바스크립트를 조금 더 이해할 수 있는 계기가 되었다.
target은 이벤트가 발생한 바로 그 요소를 직접 가리키고 currentTarget은 이벤트 리스너(EventListener)를 가진 요소를 가리킨다.
<div onClick="clicked(event)">
<span>
span
</span>
</div>
여기서 노랑은 div, 핑크는 span이며, 핑크가 노란색 위에 앉아있는 것이라고 볼 수 있다.
그런데 onClick 이벤트는 div에서 설정했지만 노랑을 눌러도, 핑크를 눌러도 모두 이벤트가 발생한다.
이것은 이벤트버블링과 관련이 있다.
이벤트 발생에 따른 target은 다음과 같다.
쉽게 말해 target은 누른 바로 그 것, currentTarget은 이벤트를 실행하는 바로 그 것으로 이해하면 된다.
즉, 우리가 핑크를 클릭했을때 핑크밑에 깔려있는 노란 부분 이벤트가 발생하지 않도록 하려면 ? target과 currentTarget을 일치하는 경우만 이벤트가 발생하도록 하자 !
(핑크는 모달이고 노랑은 모달 밖 영역이 된다!)
//모달사용하는 컴포넌트
export default function Purchase () {
const modalRef = useRef<HTMLDivElement>(null);
/// currentTarget을 지정하기 위한 useRef()
const [allSizeModalShow, setAllSizeModalShow] =useState(false)
const modalOutSideClick = (e:any) => {
if(modalRef.current === e.target) {
setAllSizeModalShow(false)
} }
///앞에서 공부한 방법
///ref는 모달 밖 영역에 걸어줌 (여기에 onClick이 있기 때문에)
///taget은 눌린 요소를 말함. 즉 모달이 눌려도 Ref.current와 다르기때문에 모달이 안닫힘!
return (
<>
<madalBtn onClick={()=>{setAllSizeModalShow(true);> 모달 열기 </madalBtn>
{allSizeModalShow &&
<Modal modalRef={modalRef} modalOutSideClick={modalOutSideClick}>}
</> /// ref와 modalOutSideClick 함수를 프롭스로 내려준다.
)}
///모달 컴포넌트
interface propsType {
modalRef: React.ForwardedRef<HTMLDivElement>;
modalOutSideClick:(e:any) => void;
}
export default function SizeSelectModal ({modalRef, modalOutSideClick}:propsType) {
return (
<AllSizeModal ref={modalRef} onClick={(e)=>modalOutSideClick(e)}>// 모달밖영역
<LayerContainer> //모달
</LayerContainer>
</AllSizeModal>
)
}
이벤트 버블링은 이벤트 발생 요소에서부터 순서대로 최상위 부모 요소까지 이벤트가 거품이 터지듯 연달아 발생한다.
<div onClick="clicked(event)" > a <div onClick="clicked(event)" > b <div onClick="clicked(event)" > c </div> </div> </div>
이 코드에서 우리가 c를 클릭한다면 c > b > a 순서대로 모두 실행된다. b를 클릭한다면 b > a 순서대로 실행된다.
쉽게 생각한다면 부모는 자식을 감싸고 있기 때문이다.