[JavaScript] 이벤트 전파 (Event Propagation)

SHY DEV·2024년 1월 16일
0

Vanilla JavaScript

목록 보기
1/3
post-thumbnail

이벤트 전파란 무엇이며, 어떻게 활용할 수 있는지 정리해 보았습니다.

이벤트 전파

  • DOM 요소에서 발생한 이벤트가 DOM Tree를 통해 전파되는 것을 의미합니다.
  • 이벤트 전파는 다음 2가지 단계로 일어납니다.
  1. 이벤트 캡쳐링 (Event Capturing)
    : 최상위 요소(html 요소)로 부터 이벤트가 일어난 요소로 이벤트가 전파되는 단계
  2. 이벤트 버블링 (Event Bubbling)
    : 이벤트가 일어난 요소로부터 최상위 요소로 이벤트가 전파되는 단계

코드로 이벤트 전파 확인하기

이벤트 전파를 확인하기 위해 간단한 화면을 하나 구성합니다.

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>EventPropagation</title>
  <Style>
    html, body, main, div, article {
      display: flex;
      align-items: center;
      justify-content: center;
    }
    body, main, div, article {
      width: 70%;
      height: 70%;
    }
    html {
      height: 100%;
      background-color: #7fffd4;
    }
    body {
      background-color: #52a666;
    }
    main {
      background-color: #22863b;
    }
    div {
      background-color: #106020
    }
  </Style>

  <!-- index.js 연동 -->
  <script src="index.js" defer></script>
</head>
<body>
  <main>
    <div>
    </div>
  </main>
</body>
</html>

// index.js
const htmlEl = document.querySelector('html');
const bodyEL = document.querySelector('body');
const mainEl = document.querySelector('main');
const divEl = document.querySelector('div');

htmlEl.addEventListener('click', function (e) {
  console.log('1. html');
});
bodyEL.addEventListener('click', function (e) {
  console.log('2. body');
});
mainEl.addEventListener('click', function (e) {
  console.log('3. main');
});
divEl.addEventListener('click', function (e) {
  console.log('4. div');
});

화면은 다음과 같습니다.

부모-자식 관계는 html > body > main > div 이고, 각 요소에 클릭시 태그명을 출력하도록 이벤트를 달아주었습니다. 이제 div 태그의 영역을 클릭하면 어떤 일이 발생할까요?

4. div
3. main
2. body
1. html

다음과 같이 div → main → body → html 순으로 등록한 이벤트가 발생하는 것을 확인할 수 있습니다.

결과 해석 : 캡쳐링, 버블링

div 요소를 클릭하면 '클릭 이벤트'는
1. html 요소로부터 시작해 div 요소의 모든 조상을 상위 요소부터 하위 요소 순으로 거쳐 내려갑니다. 이 때 캡쳐링 단계 이벤트가 등록되어있다면 해당 이벤트가 수행됩니다.
2. 실제 클릭이 일어난 div 태그에 도달하면 다시 html 요소까지 div 요소의 모든 조상을 하위 요소에서 상위 요소 순으로 거쳐 올라갑니다. 이 때 버블링 단계 이벤트가 등록되어있다면 해당 이벤트가 수행됩니다.

위에서 살펴본 예시에 div → main → body → html 순으로 이벤트가 발생한 이유는 addEventListener 함수로 인해 버블링 단계에 이벤트가 등록되었고, div태그로부터 html요소까지 이벤트가 전파되는 버블링 단계에서 각 이벤트가 수행되었기 때문입니다.
캡쳐링 단계에 이벤트를 등록하고 싶다면, addEventListener의 3번째 인자에 true를 전달하면 됩니다.

//index.js
const htmlEl = document.querySelector('html');
const bodyEL = document.querySelector('body');
const mainEl = document.querySelector('main');
const divEl = document.querySelector('div');

// addEventListener의 3번째 인자로 true 전달하여
// capturing 단계에 이벤트가 등록되도록 함
htmlEl.addEventListener('click', function (e) {
  console.log('1. html');
}, true);
bodyEL.addEventListener('click', function (e) {
  console.log('2. body');
}, true);
mainEl.addEventListener('click', function (e) {
  console.log('3. main');
}, true);
divEl.addEventListener('click', function (e) {
  console.log('4. div');
}, true);

index.js를 위와같이 수정한 후 div요소를 클릭하면 콘솔에 다음과 같이 출력됩니다.

1. html
2. body
3. main
4. div

캡쳐링 단계에 이벤트를 등록했기 때문에 html → body → main → div 순으로 이벤트가 발생하는 것을 확인할 수 있습니다.

이벤트 전파 활용하기 - 1

이벤트 전파 개념은 리스트를 다룰 때 자주 사용됩니다. ul요소 혹은 ol요소 내부의 모든 il요소에 이벤트를 등록해야 하는 상황에서 ul요소에만 이벤트를 등록하면 이벤트 전파로 인해 모든 il요소에 이벤트를 등록한 효과를 낼 수 있습니다. 다음은 간단한 예시입니다. (css는 생략하겠습니다)

<!-- index.html -->
...
<body>
  <ul class="container">
  </ul>
</body>
...

// index.js
const ulEl = document.querySelector('ul');
let items = [
  {"id": 1, "name": "apple"},
  {"id": 2, "name": "banana"},
  {"id": 3, "name": "orange"},
  {"id": 4, "name": "kiwi"},
  {"id": 5, "name": "melon"}
]

ulEl.innerHTML = items.map(item => `
  <li>
    <div class="item-id">${item.id}</div>
    <div class="item-name">${item.name}</div>
  </li>
`).join('');

과일의 id와 name 정보를 가지는 리스트를 화면에 띄우는 코드입니다.

각 리스트의 li요소에 클릭시 name을 알림창으로 띄우는 이벤트를 등록하려면 어떻게 하면 될까요? 위에서도 언급했지만, ul요소에만 이벤트를 추가해주면 됩니다.

ulEl.addEventListener('click', event => {
  // 실제 클릭된 요소(event.target)의 가장 가까운 li 요소를 찾는다.
  const liEl = event.target.closest('li');

  // li 요소 하위의 item-name을 찾아서 알림을 띄운다.
  if (!liEl || !liEl.querySelector('.item-name')) return;
  alert(liEl.querySelector('.item-name').textContent);
});

한 번의 이벤트 등록으로 모든 리스트의 아이템에 이벤트를 등록했습니다.

이벤트 전파 활용하기 - 2

앞서 언급한 예시처럼 간단한 상황만 있다면 얼마나 좋을까요? 하지만 실전에서 리스트의 아이템은 한 아이템에 여러 기능이 담겨있을 수 있습니다. 예를 들면 "찜하기" 버튼이 달려있는 상황입니다. (css는 생략하겠습니다)

const ulEl = document.querySelector('ul');
let items = [
  {"id": 1, "name": "apple"},
  {"id": 2, "name": "banana"},
  {"id": 3, "name": "orange"},
  {"id": 4, "name": "kiwi"},
  {"id": 5, "name": "melon"}
]

ulEl.innerHTML = items.map(item => `
  <li>
    <div class="item-id">${item.id}</div>
    <div class="item-name">${item.name}</div>
    <span class="favorite">찜</span>
  </li>
`).join('');

// 1. 알림창 이벤트
ulEl.addEventListener('click', event => {
  // 실제 클릭된 요소(event.target)의 가장 가까운 li 요소를 찾는다.
  const liEl = event.target.closest('li');

  // li 요소 하위의 item-name을 찾아서 알림을 띄운다.
  if (!liEl || !liEl.querySelector('.item-name')) return;
  alert(liEl.querySelector('.item-name').textContent);
});

// 2. 찜 이벤트
ulEl.addEventListener('click', event => {
  // .favorite 요소가 클릭된 경우가 아니면 종료
  if(!event.target.classList.contains('favorite')) return;

  // 찜 클릭시 active 클래스 추가, 이미 추가된 상태면 제거
  // active 클래스 추가시 글자색 빨간색이 되도록 css 작성함
  const favoriteEl = event.target;
  if (favoriteEl.classList.contains('active'))
    favoriteEl.classList.remove('active');
  else
    favoriteEl.classList.add('active');
});


의도하는 기능이 잘 작동하지만, 한 가지 문제점이 있습니다. "찜" 버튼을 클릭하면 찜 이벤트만 발생하면 좋겠는데 알림창 이벤트까지 같이 발생한다는 것이죠. 원하는 대로 동작하도록 수정하면 다음과 같습니다.

ulEl.addEventListener('click', event => {
  // 2. 찜 이벤트
  if(event.target.classList.contains('favorite')) {
    // 찜 클릭시 active 클래스 추가, 이미 추가된 상태면 제거
    // active 클래스 추가시 글자색 빨간색이 되도록 css 작성함
    const favoriteEl = event.target;
    if (favoriteEl.classList.contains('active'))
      favoriteEl.classList.remove('active');
    else
      favoriteEl.classList.add('active');
  }
  // 1. 알림창 이벤트
  else {
    // 실제 클릭된 요소(event.target)의 가장 가까운 li 요소를 찾는다.
    const liEl = event.target.closest('li');
  
    // li 요소 하위의 item-name을 찾아서 알림을 띄운다.
    if (!liEl || !liEl.querySelector('.item-name')) return;
    alert(liEl.querySelector('.item-name').textContent);
  }
});

처음부터 이렇게 짜면 되지 왜 굳이 이벤트 리스너를 2개 짜는 코드를 보여드렸냐면

function alertEventHandler (event) {
  // ...
}
function favEventHandler (event) {
  // ...
}
ulEl.addEventListener('click', alertEventHandler);
ulEl.addEventListener('click', favEventHandler);

이같이 보통 이벤트 마다 핸들러 함수를 따로 정의하여 사용하기 때문입니다. 여러분은 그럼에도 원하는 대로 코드를 잘 짜시겠지만 사실은 제가 프로젝트에 이 개념을 적용하다 실수했던 부분이기 때문에 언급해보았습니다.

프로젝트에 적용했던 경험


위와 같이 클라이밍 매장의 정보를 리스트로 보여주는 페이지를 제작한 적 있습니다. 해당 매장의 카드를 클릭하면 상세페이지로 넘어가고, 좋아요 버튼을 클릭하면 좋아요 활성화 혹은 해제되는 기능을 구현하였습니다.
그러나 '이벤트 전파 활용하기 - 2'의 첫부분에서 언급한 실수와 같이 따로 이벤트 리스너를 만들어 등록하였고, 좋아요 버튼을 누르면 좋아요 활성화/해제와 동시에 상세페이지로 넘어가는 상황이 펼쳐져 해결하는데 애를 먹었습니다.
해당 경험을 한 후, 본 개념을 정리해야겠다는 결심을 하여 글을 쓰게 되었습니다.

profile
서로의 발전을 위해 정리하고 공유합니다. 피드백 환영합니다.

0개의 댓글

관련 채용 정보