오늘은 이벤트 위임을 현재 진행중인 grand busan 클론코딩 프로젝트에 적용시켜보려고 한다.
현재 내 프로젝트의 Header 컴포넌트의 메뉴 리스트 구성은 이렇게 되어있다.
//Header.js
//...
<HeaderList>
{listArray.map((listItem, index) => {
return (
<HeaderListItem key={index} isOn={isOn} id={index}>
<a
href="/"
onMouseEnter={(e) => {
setIsHover(true);
setIsOn(index);
}}
>
{listItem.listItem}
</a>
{subMenuFn(listItem)}
</HeaderListItem>
);
})}
</HeaderList>
//...
HeaderList라는 ul태그 안에 HeaderListItem이라는 li태그를 map으로 생성하고, li태그 하나하나에 onMouseEnter 이벤트 핸들러를 적용시키고 있는 모습이다.
나는 이 코드에 큰 의문을 느끼지 못 했다... 이벤트 위임을 알기 전까진 말이다!
이벤트 위임이란 무엇인가?
이벤트 캡쳐링과 이벤트 버블링을 이용해서, 하나의 조상요소에서 그 하위요소들의 이벤트를 모두 컨트롤하는 것을 이벤트 위임이라고 할 수 있다. 즉, 하위요소가 조상요소에게 이벤트를 위임하는 것이다.
그럼 내 코드를 리팩토링하며 더 자세히 알아보자.
//Header.js
//...
<ul>
{listArray.map((listItem, index) => {
return (
<li key={index} isOn={isOn} id={index}>
<a
href="/"
onMouseEnter={(e) => {
setIsHover(true);
setIsOn(index);
}}
>
{listItem.listItem}
</a>
{subMenuFn(listItem)}
</ul>
);
})}
</li>
//...
이해를 돕기 위해 컴포넌트 명들을 HTML 태그명으로 변경했다.
이벤트 위임을 알고 난 후 이 코드를 다시보니 '이런 더러운 코드가 다 있나!' 싶다.
a 태그의 갯수만큼 onMouseEnter의 이벤트 리스너의 갯수도 늘어나고, 그만큼 리소스를 잡아먹고 있는 것이다. 그럼 이제 이벤트 위임을 적용해보겠다.
//Header.js
//...
<ul
onMouseEnter={(e) => {
setIsHover(true);
console.log(e.target);
}}
>
{listArray.map((listItem, index) => {
return (
<li key={index} isOn={isOn} id={index}>
<a href="/">{listItem.listItem}</a>
{subMenuFn(listItem)}
</li>
);
})}
</ul>
//...
우선 a 태그에 붙어있던 onMouseEnter를 제거했고 반복되는 li들의 공통 조상요소인 ul에 onMouseEnter 이벤트 리스너를 달아주었다. 이제 a태그에 mouseEnter 이벤트가 일어나면 어떻게 될까?
보다시피 a 태그에는 이벤트리스너가 없음에도 불구하고 ul 요소에서 이벤트를 감지하여 console.log(e.target) 을 실행해주고 있다.
당연한 이야기다. ul 태그에 이벤트리스너를 등록했기 때문에, ul 태그 자체에서 일어나는 이벤트도 감지된다. 위의 사진은 ul 태그의 padding에 onMouseEnter 되었을 때의 모습이다.
이 문제는 user agent stylesheet의 ul태그의 padding값을 0으로 초기화하고 margin-left를 더 줘서 해결했다.
a 태그에서 다른 a 태그로 마우스를 옮길 때 onMouseEnter이벤트가 감지되질 않는다. a 태그에 onMouseEnter 이벤트가 일어날 때 마다 ul 태그의 이벤트 리스너가 감지해야 하는 것이 아닌가?
라고 생각했었다. onMouse 이벤트에 관한 글을 읽기 전 까지는!
onMouseEnter 이벤트는 요소 밖으로 포인터가 나갔다 오지 않으면 계속 발생하지 않는다. 그리고 onMouseOver 이벤트는 요소 영역을 벗어나지 않아도 자식 요소에 들어갈 때에 발생한다.
즉 위의 경우에는 onMouseOver 이벤트 리스너를 달아주어야 한다는 것이다.
onMouseOver로 바꿔주자 감지가 잘 되는 모습
남은 일은 mouseOver된 a 요소의 부모인 li 요소의 index값을 읽어와서 setIsOn(index) 해주는 것 뿐이다.
<ul
onMouseOver={(e) => {
setIsHover(true);
setIsOn(parseInt(e.target.parentElement.id));
// isOn과 같은 id를 가지고 있는 li의 SubMenu 컴포넌트가 보여진다.
}}
>
{listArray.map((listItem, index) => {
return (
<li key={index} isOn={isOn} id={index}>
<a href="/">{listItem.listItem}</a>
{subMenuFn(listItem)}
</li>
);
})}
</ul>
적용이 완료되었다!
요 근래 프론트엔드 기술 면접에 관련된 글들을 찾아보면서 그동안 내가 얼마나 프론트엔드에 대해 무지했는지 깨우치고 있다. 이벤트 위임이란 개념도 글을 읽기 전까지는 알지 못 했었다.
프론트엔드 개발자가 갖춰야 마땅한 지식임에도 까맣게 모르고 있었다는 사실이 부끄러웠고, 한 편으로는 이제 알게 되어서 굉장히 기쁘다.
또 이렇게 블로그에 공부한 내용을 게시하며 배워나가니 더 정확하고 꼼꼼하게 배우게 된다. 좋은 흐름이다. 이 흐름에 맞추어 앞으로 걸어나가겠다.