모달을 구현할 때 필연적으로 마주하는 문제가 있다.
바로 이벤트 버블링이라는 현상이다.
이벤트 버블링이란?
한 요소에 이벤트가 발생하면, 이 요소에 할당된 핸들러가 동작하고, 이어서 부모 요소의 핸들러가 동작합니다. 가장 최상단의 조상 요소를 만날 때까지 이 과정이 반복되면서 요소 각각에 할당된 핸들러가 동작합니다.
(https://ko.javascript.info/bubbling-and-capturing)
쉽게 예시로 확인해 보자.
위 모달은 크게 두 가지 영역으로 구분된다.
1) 모달 배경
2) 모달 내용
모달 내용 부분에는 모달에 필요한 정보들을 담고 있으며, 필요에 따라 다양한 기능을 수행한다. 위 예시에는 모달 닫기 버튼 외에는 기능이 없다.
모달을 표시했다면, 원래의 내용으로 돌아가기 위해 모달을 닫는 기능도 필요한데, 위 예시처럼 버튼으로 구현하거나, X 표시 등으로 구현할 수 있다. 이 때, 편의를 위해 1번 영역인 모달 배경 부분을 클릭(터치)할 경우 모달을 닫는 기능을 포함할 수 있다.
배경 터치 시 모달을 닫는 기능을 구현해 보자.
const [modalVisible, setModalVisible] = useState(false);
return (
<View style={styles.centeredView}>
<Modal
animationType="slide"
transparent={false}
visible={modalVisible}
onRequestClose={() => {
Alert.alert('Modal has been closed.');
setModalVisible(!modalVisible);
}}
>
<View // 얘가 모달 배경임
style={styles.centeredView}
onTouchEnd={() => setModalVisible(false)} // 터치하면 모달 끔
>
<View style={styles.modalView}>
<Text style={styles.modalText}>나 모달이유~</Text>
<TouchableOpacity
style={[styles.button, styles.buttonClose]}
onPress={() => setModalVisible(!modalVisible)}>
<Text style={styles.textStyle}>모달 사라져</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
<TouchableOpacity
style={[styles.button, styles.buttonOpen]}
onPress={() => setModalVisible(true)}>
<Text style={styles.textStyle}>모달 나와</Text>
</TouchableOpacity>
</View>
);
}
주석으로 표시된 부분이 해당 기능을 구현한 코드이다. 이제 배경을 클릭하면 바탕으로 돌아간다.
바로 지금 이벤트 버블링이라는 문제가 발생한다.
의도한 코드는 1번 공간을 터치할 경우에만 모달이 꺼지는 것인데, 2번 공간을 터치하여도 모달이 꺼지는 현상이 발생한다.
해당 현상의 원인은 위에서 언급한 내용과 같다.
한 요소에 이벤트가 발생하면, 이 요소에 할당된 핸들러가 동작하고, 이어서 부모 요소의 핸들러가 동작하기 때문.
위 규칙에 의해
모달 내용 부분의 핸들러가 동작 -> 모달 배경 부분의 핸들러가 동작
즉, 모달 내용 부분이 모달 배경 부분의 자식 컴포넌트이기에 발생한다.
이러한 현상을 해결할 수 있는 다양한 방법이 있겠지만 아주 간단하게 버블링을 막을 수 있다.
바로 stopPropagation 메서드를 사용하는 것이다.
stopPropagation()인터페이스 의 메서드는 캡처 Event 및 버블링 단계에서 현재 이벤트의 추가 전파를 방지합니다.
(https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation)
해당 설명에, 버블링 단계에서 '현재' 이벤트의 추가 전파를 방지한다고 기술되어 있으므로 stopPropagation을 사용해야 할 이벤트의 컴포넌트는 자식 컴포넌트인 모달 내용 부분이다.
버블링은 해당 요소의 핸들러 -> 부모 요소의 핸들러로 전파되기 때문이다.
코드로 작성해 보자.
const [modalVisible, setModalVisible] = useState(false);
return (
<View style={styles.centeredView}>
<Modal
animationType="slide"
transparent={false}
visible={modalVisible}
onRequestClose={() => {
Alert.alert('Modal has been closed.');
setModalVisible(!modalVisible);
}}
>
<View // 얘가 모달 배경임
style={styles.centeredView}
onTouchEnd={() => setModalVisible(false)}
>
<View // 이노무자슥!
style={styles.modalView}
onTouchEnd={(e) => e.stopPropagation()} //ez...
>
<Text style={styles.modalText}>나 모달이유~</Text>
<TouchableOpacity
style={[styles.button, styles.buttonClose]}
onPress={() => setModalVisible(!modalVisible)}>
<Text style={styles.textStyle}>모달 사라져</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
<TouchableOpacity
style={[styles.button, styles.buttonOpen]}
onPress={() => setModalVisible(true)}>
<Text style={styles.textStyle}>모달 나와</Text>
</TouchableOpacity>
</View>
);
}
자식 컴포넌트(모달 내용 부분)에서 이벤트 발생 시 stopPropagation 메서드를 사용하니 배경과 닫기 버튼을 클릭했을 때만 모달이 닫히는 것을 확인할 수 있다. 끝!