프로젝트 중 카드 오른쪽 위 케밥 아이콘을 누르면 메뉴가 보이는 기능을 넣어야 했다.
그런데 문제. 카드를 누르면 해당 글 상세 페이지로 넘어가게 되어 있는데 아이콘을 눌러도 페이지가 넘어가는 일이 발생했다.
대충 이벤트 전파 관련인줄은 알았지만, 정확히 모르고 있어서 이번 기회를 통해 공부했음
이벤트 전파
DOM 요소 노드에서 발생한 이벤트는 DOM 트리를 통해 전파된다. 이를 이벤트 전파라고 한다.
표준 DOM 이벤트에서 정의한 이벤트 흐름엔 다음 3가지 단계가 있다.
1. 캡처링 단계: 이벤트가 하위 요소로 전파
2. 타깃 단계: 이벤트가 실제 타깃 요소에 도달
3. 버블링 단계: 이벤트가 상위 요소로 전파
보통 어트리뷰트/프로퍼티 방식으로 등록한 이벤트 핸들러는 타겟 단계와 버블링 단계의 이벤트만 실행된다.
굳이 캡처링 단계를 캐치하고 싶다면 elem.addEventListener( ..., { capture: true } ) 또는 elem.addEventListener(..., true) 를 사용하여 선별적으로 사용할 수 있다.
예외로 이벤트 버블링으로 전파되지 않는 이벤트가 있다.
- focus/blur
- load/unload/abort/error
- mouseenter/mouseleave
버블링이 반드시 필요하다면 다음으로 대체할 수 있다.
- focus/blur -> focusin/focusout
- mouseenter/mouseleave -> mouseover/mouseout
해결법
프로젝트로 돌아와서 나의 상황에서는 Card 컴포넌트 안에 자식으로 Menu Icon 컴포넌트가 들어있고, 두 컴포넌트 모두 클릭 이벤트 핸들러가 등록되어 이벤트 버블링이 생긴 것이다.
가장 간단한 해결법으로 event.stopPropagation() 메서드가 있다. 다음 코드로 에러를 해결했다.
const handleClickIcon = (e: MouseEvent) => {
e.stopPropagation();
};
일단 이렇게 해결하고 프로젝트가 끝났지만, 포스팅 하는 과정에서 모던 JS 튜토리얼을 보니 이 방법을 왠만하면 사용하지 말라고 한다. 이유는 stopPropagation을 사용한 영역은 "죽은 영역"이 되는데, 이 죽은 영역은 클릭 이벤트 감지가 불가능하기 때문에 만약 유저 행동 패턴을 분석하기 위해 클릭 이벤트 감지가 필요할 때 분석에 방해가 될 수 있기 때문이다. 또한 상위 요소의 모든 이벤트가 막혀서 이벤트 로직이 복잡할 경우 의도치 않은 에러가 발생할 수도 있다.
나의 경우 카드와 아이콘을 형제로 두고 상위 태그를 하나 더 감싼 후, position 속성을 활용하여 우회할 수 있다.
이렇게 하면 부모-자식 관계가 아니라서 이벤트 버블링에 의한 페이지 이동이 일어나지 않는다.
// 기존 코드
return (
<Card>
...
<MenuIcon />
</Card>
)
// 변경 코드
return (
<div style={{ position: "relative" }}>
<Card />
<MenuIcon style={{ position: "absolute", top: "8px", right: "8px" }} />
</div>
)
실제로 적용해본 결과
- stopPropagation 없이 잘 작동된다.
- 하지만 hover 애니메이션 이슈로 인한 CSS 변경, Wrapper 태그 증가 등 단점도 있었음.
- 사실 클릭 이벤트가 복잡하게 구성되지 않았고, 유저 행동 패턴 분석이 필요한 프로젝트가 아니라고 생각이 들어서 해당 프로젝트에서는 중요성을 느끼진 못했다.
- 추가적으로... 원래 이벤트 핸들러를 사용해서 useNavigate로 페이지 이동을 시켰었는데, Link 컴포넌트에서도 stopPropagation를 사용하면 페이지가 이동되지 않았음
참고자료