WeniVooks

검색

JavaScript 에센셜

DOM의 이벤트 흐름

1. 이벤트 흐름

브라우저 화면에서 이벤트가 발생하면 브라우저는 가장 먼저 이벤트 대상을 찾기 시작합니다.

브라우저가 이벤트 대상을 찾아갈 때는 가장 상위의 window 객체부터 document, body 순으로 DOM 트리를 따라 내려갑니다. 이를 캡처링 단계라고 합니다.

이때 이벤트 대상을 찾아가는 과정에서 브라우저는 중간에 만나는 모든 캡처링 이벤트 리스너를 실행시킵니다. 그리고 이벤트 대상을 찾고 캡처링이 끝나면 이제 다시 DOM 트리를 따라 올라가며 만나는 모든 버블링 이벤트 리스너를 실행합니다. 이를 이벤트 버블링 단계라고 합니다.

즉, 캡처링 단계 -> 이벤트 대상 -> 버블링 단계 순으로 이벤트가 전파됩니다. 전파되는 과정에서 이벤트 리스너가 차례로 실행되는것을 이벤트 전파(event propagation)라고 합니다.

다음 코드를 태그에 추가하고 버튼을 클릭해보세요.

<article class="parent">
  <button class="btn" type="button">버튼</button>
</article>
 
<script>
  const parent = document.querySelector('.parent');
  const btnFirst = document.querySelector('.btn');
  btnFirst.addEventListener('click', () => {
    console.log('btn capture!');
  });
 
  window.addEventListener(
    'click',
    () => {
      console.log('window capture!');
    },
    true,
  ); // true : 캡처링 단계의 이벤트가 발생하도록 합니다.
 
  document.addEventListener(
    'click',
    () => {
      console.log('document capture!');
    },
    true,
  );
 
  parent.addEventListener(
    'click',
    () => {
      console.log('parent capture!');
    },
    true,
  );
 
  btnFirst.addEventListener('click', () => {
    console.log('btn bubble!');
  });
 
  parent.addEventListener('click', () => {
    console.log('parent bubble!');
  });
 
  document.addEventListener('click', () => {
    console.log('document bubble!');
  });
 
  window.addEventListener('click', () => {
    console.log('window bubble!');
  });
</script>
<article class="parent">
  <button class="btn" type="button">버튼</button>
</article>
 
<script>
  const parent = document.querySelector('.parent');
  const btnFirst = document.querySelector('.btn');
  btnFirst.addEventListener('click', () => {
    console.log('btn capture!');
  });
 
  window.addEventListener(
    'click',
    () => {
      console.log('window capture!');
    },
    true,
  ); // true : 캡처링 단계의 이벤트가 발생하도록 합니다.
 
  document.addEventListener(
    'click',
    () => {
      console.log('document capture!');
    },
    true,
  );
 
  parent.addEventListener(
    'click',
    () => {
      console.log('parent capture!');
    },
    true,
  );
 
  btnFirst.addEventListener('click', () => {
    console.log('btn bubble!');
  });
 
  parent.addEventListener('click', () => {
    console.log('parent bubble!');
  });
 
  document.addEventListener('click', () => {
    console.log('document bubble!');
  });
 
  window.addEventListener('click', () => {
    console.log('window bubble!');
  });
</script>

실행하면 window → document → body → article → button 순으로 등록된 캡쳐링 이벤트가 발생하고, button → article → body → document → window 순으로 등록된 버블링 이벤트가 발생하는 것을 확인할 수 있습니다.

2. 이벤트 객체

이벤트에서 호출되는 핸들러에는 이벤트와 관련된 모든 정보를 가지고 있는 매개변수가 전송됩니다. 이것이 바로 이벤트 객체입니다.

<article class="parent">
  <ol>
    <li><button class="btn-first" type="button">버튼1</button></li>
    <li><button type="button">버튼2</button></li>
    <li><button type="button">버튼3</button></li>
  </ol>
</article>
 
<script>
  const parent = document.querySelector('.parent');
  parent.addEventListener('click', function (event) {
    console.log(event);
  });
</script>
<article class="parent">
  <ol>
    <li><button class="btn-first" type="button">버튼1</button></li>
    <li><button type="button">버튼2</button></li>
    <li><button type="button">버튼3</button></li>
  </ol>
</article>
 
<script>
  const parent = document.querySelector('.parent');
  parent.addEventListener('click', function (event) {
    console.log(event);
  });
</script>

부모부터 자식까지 일련의 요소를 모두 타고가며 진행되는 이벤트 전파로 인해 이벤트 객체에는 target, currentTarget 이라는 속성이 존재합니다. 두 속성을 통해 이벤트가 발생한 요소와 이벤트 리스너가 연결된 요소를 구분할 수 있습니다.

  • target: 이벤트가 발생한 진원지의 정보가 담겨 있습니다. target 속성을 통해 이벤트 리스너가 없는 요소의 이벤트가 발생했을 때도 해당 요소에 접근 할 수 있습니다.
  • currentTarget: 이벤트 리스너가 연결된 요소가 참조되어 있습니다.
<article class="parent">
  <ol>
    <li><button class="btn-first" type="button">버튼1</button></li>
    <li><button type="button">버튼2</button></li>
    <li><button type="button">버튼3</button></li>
  </ol>
</article>
 
<script>
  const parent = document.querySelector('.parent');
  parent.addEventListener('click', function (event) {
    console.log(event.target);
    console.log(event.currentTarget); // 이벤트 리스너가 연결된 요소 (parent)
  });
</script>
<article class="parent">
  <ol>
    <li><button class="btn-first" type="button">버튼1</button></li>
    <li><button type="button">버튼2</button></li>
    <li><button type="button">버튼3</button></li>
  </ol>
</article>
 
<script>
  const parent = document.querySelector('.parent');
  parent.addEventListener('click', function (event) {
    console.log(event.target);
    console.log(event.currentTarget); // 이벤트 리스너가 연결된 요소 (parent)
  });
</script>

3. 이벤트 위임

이벤트 흐름과 이벤트 객체의 속성을 통해 이벤트 리스너가 없는 요소의 이벤트가 발생했을 때도 해당 요소에 접근 할 수 있습니다. 이러한 특징을 활용하여 부모 요소에 이벤트 리스너를 등록하고, 이벤트 객체의 target 속성을 통해 이벤트가 발생한 요소를 찾아내는 방법을 사용할 수 있습니다. 이렇게 하면 부모 요소에 이벤트 리스너를 하나만 등록해도 자식 요소의 이벤트를 모두 감지할 수 있습니다. 이를 이벤트 위임(event delegation) 이라고 합니다.

<body>
  <article class="parent">
    <ol>
      <li><button class="btn-first" type="button">버튼1</button></li>
      <li><button type="button">버튼2</button></li>
      <li><button type="button">버튼3</button></li>
    </ol>
  </article>
 
  <script>
    const parent = document.querySelector('.parent');
    parent.addEventListener('click', function (event) {
      console.log(event.target);
      if (event.target.nodeName === 'BUTTON') {
        event.target.textContent = '버튼4';
      }
    });
  </script>
</body>
<body>
  <article class="parent">
    <ol>
      <li><button class="btn-first" type="button">버튼1</button></li>
      <li><button type="button">버튼2</button></li>
      <li><button type="button">버튼3</button></li>
    </ol>
  </article>
 
  <script>
    const parent = document.querySelector('.parent');
    parent.addEventListener('click', function (event) {
      console.log(event.target);
      if (event.target.nodeName === 'BUTTON') {
        event.target.textContent = '버튼4';
      }
    });
  </script>
</body>

4. 이벤트 흐름 조작

4.1. stopPropagation()

브라우저에서 이벤트가 기본적으로 캡처링과 버블링 단계를 거치면서 전파됩니다. 이 과정에서 이벤트가 전파되는 것을 막기 위해 stopPropagation()을 사용합니다. 이벤트 핸들러에서 stopPropagation을 만나면 이벤트 전파가 중지됩니다.

다음코드에서 주석을 해제하면서 stopPropagation이 어떻게 동작하는지 알아보세요.

<article class="parent">
  <button class="btn" type="button">버튼</button>
</article>
 
<script>
  const parent = document.querySelector('.parent');
  const btnFirst = document.querySelector('.btn');
 
  btnFirst.addEventListener(
    'click',
    (e) => {
      // e.stopPropagation();
      console.log('btn capture!');
    },
    true,
  );
 
  parent.addEventListener(
    'click',
    (e) => {
      // e.stopPropagation();
      console.log('parent capture!');
    },
    true,
  );
 
  btnFirst.addEventListener('click', (e) => {
    // e.stopPropagation();
    console.log('btn bubble!');
  });
 
  parent.addEventListener('click', (e) => {
    // e.stopPropagation();
    console.log('parent bubble!');
  });
</script>
<article class="parent">
  <button class="btn" type="button">버튼</button>
</article>
 
<script>
  const parent = document.querySelector('.parent');
  const btnFirst = document.querySelector('.btn');
 
  btnFirst.addEventListener(
    'click',
    (e) => {
      // e.stopPropagation();
      console.log('btn capture!');
    },
    true,
  );
 
  parent.addEventListener(
    'click',
    (e) => {
      // e.stopPropagation();
      console.log('parent capture!');
    },
    true,
  );
 
  btnFirst.addEventListener('click', (e) => {
    // e.stopPropagation();
    console.log('btn bubble!');
  });
 
  parent.addEventListener('click', (e) => {
    // e.stopPropagation();
    console.log('parent bubble!');
  });
</script>
4.2. preventDefault()

stopPropagation을 이용하더라도 브라우저의 기본 동작은 중지되지 않습니다. HTML 태그에서 제공하는 기본 기능이 실행되는 것을 막기 위해서는 preventDefault()를 사용합니다.

<!-- 앵커의 기본 동작을 중지 -->
<a href="https://paullab.co.kr/" class="link">제주코딩베이스캠프</a>
<script>
  const link = document.querySelector('.link');
  link.addEventListener('click', (event) => {
    event.preventDefault();
    console.log('link clicked');
  });
</script>
<!-- 앵커의 기본 동작을 중지 -->
<a href="https://paullab.co.kr/" class="link">제주코딩베이스캠프</a>
<script>
  const link = document.querySelector('.link');
  link.addEventListener('click', (event) => {
    event.preventDefault();
    console.log('link clicked');
  });
</script>
<!-- submit의 기본 동작을 중지 -->
<form action="">
  <button type="submit" class="submit">제출</button>
</form>
<script>
  const submit = document.querySelector('.submit');
  submit.addEventListener('click', (event) => {
    event.preventDefault();
    console.log('form submit');
  });
</script>
<!-- submit의 기본 동작을 중지 -->
<form action="">
  <button type="submit" class="submit">제출</button>
</form>
<script>
  const submit = document.querySelector('.submit');
  submit.addEventListener('click', (event) => {
    event.preventDefault();
    console.log('form submit');
  });
</script>

이렇듯 종종 브라우저의 기본 동작을 중지하고 자바스크립트를 통해 기능을 처리하고자 할때 사용합니다.

5. 이벤트의 this

이벤트 핸들러 내부에서 this는 이벤트 핸들러가 등록되어 있는 요소를 참조합니다. 이벤트 객체의 currentTarget 속성의 참조값과 동일합니다.

<nav class="menu-nav">
  <ul>
    <li><button type="button">버튼1</button></li>
    <li><button type="button">버튼2</button></li>
    <li><button type="button">버튼3</button></li>
  </ul>
</nav>
 
<script>
  const menu = document.querySelector('.menu-nav');
  menu.addEventListener('click', function (event) {
    console.log(this);
  });
</script>
<nav class="menu-nav">
  <ul>
    <li><button type="button">버튼1</button></li>
    <li><button type="button">버튼2</button></li>
    <li><button type="button">버튼3</button></li>
  </ul>
</nav>
 
<script>
  const menu = document.querySelector('.menu-nav');
  menu.addEventListener('click', function (event) {
    console.log(this);
  });
</script>

화살표 함수와 this
화살표 함수를 사용해 이벤트 핸들러를 정의했을 때, this는 이벤트가 발생한 요소를 참조하지 않습니다. 화살표 함수는 자신만의 this를 가지지 않기 때문입니다. 화살표 함수를 둘러싸고 있는 스코프의 this를 가리킵니다.

<button id="myButton">Click me</button>
 
<script>
  document.getElementById('myButton').addEventListener('click', () => {
    console.log(this); // 상위 스코프의 this(window)를 가리킴
  });
</script>
<button id="myButton">Click me</button>
 
<script>
  document.getElementById('myButton').addEventListener('click', () => {
    console.log(this); // 상위 스코프의 this(window)를 가리킴
  });
</script>
10.2 DOM 제어하기11장 객체지향 프로그래밍