📍 목표: 모달창이 켜진 상태에서 모달창 영역을 클릭할 때는 모달창이 유지되고, 박스 바깥 영역(dimmed layer)을 클릭할 때는 모달창이 닫히게 하기
코드 컴포넌트 설명
- <ModalContainer> : 컨테이너
- <ModalBtn> : Open Modal 이라고 써진 버튼. 클릭시 모달창 열리고 버튼 안의 텍스트 opend! 로 변경됨
- <ModalBackdrop> : 모달창 오픈 시 화면 전체 배경에 깔리는 dimmed layer
- <ModalView> : 모달창 영역(흰색 박스)
- <ModalCloseBtn> : 모달창의 x자 버튼. 클릭시 모달창 닫힘
export const Modal = () => {
const [isOpen, setIsOpen] = useState(false);
const openModalHandler = () => {
setIsOpen((cur) => !cur);
};
return (
<>
<ModalContainer>
<ModalBtn onClick={openModalHandler}>
{isOpen ? "Opened!" : "Open Modal"}
</ModalBtn>
{isOpen ? (
<ModalBackdrop>
<ModalView>
<CloseBtn onClick={openModalHandler}>X</CloseBtn>
<p>Hello Codestates!</p>
</ModalView>
</ModalBackdrop>
) : null}
</ModalContainer>
</>
);
};
현재 상태에서는 모달창 안의 x 버튼을 눌리면 모달이 닫힌다. x 버튼 외에 모달 영역 바깥(dimmed layer)을 클릭해도 모달이 닫히도록 구현하기 위해서는 두 가지 방법이 있다.
1. 이벤트 전파(Event Propagation) 방지
단순하게 모달 영역 바깥인 <ModalBackdrop> 에 클릭 이벤트를 하나 더 걸어주면 되는거 아냐? 라고 생각할 수 있다. 그리고 그렇게 이벤트를 걸고 나면 모달 영역 밖을 클릭했을 때 모달이 닫히긴 하지만, 문제는 모달창을 클릭해도 모달이 닫히고, 모달창의 x자 버튼이 작동하지 않는다.
이벤트의 캡처링(자식 요소에서 발생한 이벤트가 부모 요소부터 시작해 이벤트를 발생시킨 자식 요소까지 도달하는 것)과 버블링(자식 요소에서 발생한 이벤트가 부모 요소로 전파되는 것)이라고 하는 이벤트 전파 특성 때문에 생기는 문제이다. 지금 잘 이해가 되지 않는 부분은 버블링은 자식 요소의 이벤트가 부모 요소로 전파되는 것인데, 위의 코드의 경우 Backdrop이 부모 컴포넌트이고, 그 자식 컴포넌트인 ModalView에서 동일한 이벤트가 마치 상속된 것처럼 나타나고 있는 것인데...? 이것도 버블링이라고 볼 수 있는 것인지?
여하튼 이러한 원하지 않는 동작을 막는 방법은 간단하다. 이벤트가 전파되지 않기를 원하는 곳, 즉 이벤트가 실행되면 안 되는 곳에 event.stopPropagation 메소드를 적용해 주면 된다.
[2022. 12. 25. 추가]
이벤트 전파에 있어서 전파되는 것은 '이벤트 핸들러 함수'가 아닌 '이벤트 자체'라는 것을 혼동했다. 마치 자식 컴포넌트인 ModalView에 부모 컴포넌트 BackDrop의 이벤트 핸들러 함수가 상속된 것처럼 보이지만, 실제로는 이벤트 핸들러 함수를 받아서 실행한 것이 아니라 '클릭 이벤트'가 이벤트 버블링으로 인해 부모 컴포넌트까지 타고 올라가서 부모 컴포넌트의 onClick 이벤트 트리거가 된 것이다.
모달창의 x자 버튼이 작동하지 않았던 이유를 파악하기 위해서 아래와 같이 이벤트 핸들러에 clicked를 콘솔에 찍어주는 코드와 isOpen의 상태값을 확인할 수 있는 코드를 추가하였다.
const openModalHandler = () => {
console.log("clicked");
setIsOpen((cur) => !cur);
};
console.log("isOpen: " + isOpen);
위의 코드를 추가하고 모달창을 열고 닫았을 때 콘솔에 다음과 같은 상태가 찍혔다.
모달 박스 안의 x자 닫힘 버튼을 눌러도 창이 닫히지 않은 이유는 다음과 같다.
1) closeBtn 컴포넌트를 클릭했을 때 '클릭 이벤트'가 버블링으로 인해 부모의 부모인 BackDrop에까지 전파되었고,
2) closeBtn과 BackDrop에 걸려있는 동일한 이벤트 핸들러 openModalHandler 함수가 두 번 호출되었다. (콘솔창에 clicked가 두 번 찍힌 이유)
3) setIsOpen 메소드도 두 번 호출되어서 isOpen 상태값이 첫 번째 호출에서 false, 두 번째 호출에서 true로 바뀜 (state 변경을 함수형 업데이트로 했기 때문)
4) 리액트가 이벤트 핸들러를 처리할 때 배칭batching을 하기 때문에 리렌더링은 상태값 변경을 모아서 한 번만 진행됨 (콘솔에 isOpen: true 하나만 찍힌 이유)
이때 아래와 같이 ModalView 컴포넌트에 stopPropagation 처리를 해주면 해당 컴포넌트의 부모인 BackDrop의 이벤트 전파도 막아주고, 자식인 CloseBtn의 이벤트 전파도 같이 막아주기 때문에 각 컴포넌트가 자신의 클릭 이벤트에만 이벤트 핸들러 함수를 호출하게 된다.
export const Modal = () => {
// 생략
return (
<>
// 생략
<ModalBackdrop onClick={openModalHandler}>
<ModalView onClick={(e) => e.stopPropagation()}>
<ModalCloseBtn onClick={openModalHandler}>X</ModalCloseBtn>
<p>Hello Codestates!</p>
</ModalView>
</ModalBackdrop>
// 생략
</>
);
};
2. useRef hook
모달창 바깥 영역 컴포넌트인 <ModalBackdrop>을 useRef를 사용해 선택한 후, 해당 영역을 클릭 시에만 모달창이 닫히도록 이벤트를 걸어준다.
export const Modal = () => {
const outside = useRef();
// 생략
return (
<>
// 생략
<ModalBackdrop
ref={outside}
onClick={(e) => {
if (outside.current === e.target) {
openModalHandler();
}
}}
>
<ModalView>
<ModalCloseBtn onClick={openModalHandler}>X</ModalCloseBtn>
<p>Hello Codestates!</p>
</ModalView>
</ModalBackdrop>
// 생략
</>
);
};
📚 참고자료
'배워서 남 주자' 카테고리의 다른 글
input 엔터키 입력 감지해서 이벤트 실행하는 방법 (0) | 2022.12.26 |
---|---|
[React] 이벤트 핸들러에 argument 전달하는 방법 (0) | 2022.12.26 |
[JavaScript] 순수 함수란 무엇인가? (0) | 2022.12.14 |
바닐라JS로 fetch API를 사용해 서버에 요청한 데이터 받아오는 방법 (0) | 2022.12.13 |
[JavaScript] 문자열(string)을 날짜로 바꾸는 방법 (0) | 2022.12.13 |