Programming/JS

이벤트 전파 단계와 이벤트 위임 - 폼(form)에 버블링🫧 활용하기

soyeori 2023. 10. 3. 14:57

최근 자바스크립트 '이벤트(event)'를 다시 공부하면서 이벤트 전파에 대한 개념을 다시 한번 정리하는 시간을 가졌다. 또한 이벤트 전파와 더불어 이벤트 위임에 관한 개념도 더 확실히 알게 되어 글로 정리하게 되었다.

 

🫧 이벤트 전파 - 버블링과 캡쳐링

🫧 이벤트 위임

🛠️ 위 개념을 바탕으로 로그인 페이지 폼양식을 이벤트 버블링 개념을 활용하여 리팩토링 하기

이벤트 전파

DOM 트리의 요소 노드에서 발생한 이벤트는 DOM 트리를 통해 전파된다. 즉, 이벤트가 발생했을 때 생성된 이벤트 객체는 이벤트 타깃(event target)을 중심으로 DOM 트리를 통해서 전파되는데 전파 방향에 따라 캡쳐링과 버블링으로 구분된다.

 

 

✅ 이벤트 캡쳐링 (capturing phase)

 

이벤트가 상위 요소에서 하위 요소 방향으로 전파되는 단계이다. 참고로 캡쳐링 단계를 이용해야 하는 경우는 흔하지 않아서 실제 코드에서 잘 쓰이지 않지만 캡쳐링과 버블링은 모두 이벤트 위임을 이해하는데 토대가 되므로 알아둘 필요가 있다.

 

이벤트 핸들러를 등록할 때 어트리뷰트 또는 프로퍼티 방식으로 등록한다면 해당 이벤트 핸들러는 타깃 단계(이벤트가 이벤트 타깃에 도달)와 버블링 단계만 확인할 수 있다. 그러므로 이벤트 캡쳐를 사용하려면 addEventListener 방식으로 이벤트를 등록하고 메서드의 3번째 인수로 true를 전달해줘야 한다. 

<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- 생략 -->
    <title>이벤트 캡쳐링</title>
    <style>
	<!-- 생략 -->
    </style>
  </head>
  <body>
    <div class="upper" id="1">1
      <div class="middle" id="2">2
        <div class="lower" id="3">3</div>
      </div>
    </div>
    <script>
      const divs = document.querySelectorAll("div");
      divs.forEach((div) => {
        div.addEventListener(
          "click",
          (e) => alert(e.currentTarget.getAttribute("id")),
          true
        );
      });
    </script>
  </body>
</html>

3번을 클릭하면 1 → 2 → 3 순으로 이벤트가 전파되는 것을 알 수 있다.

 

참고로 이벤트 객체의 프로퍼티로 target과 currentTarget 은 각각 DOM 요소 노드에 접근할 수 있다.

  ✔️ target : 이벤트를 발생시킨 요소, 클릭 이벤트라면 클릭을 누른 요소

  ✔️ currentTarget : 이벤트 핸들러가 바인딩된 요소로 이벤트를 실행하는 즉, 이벤트 리스너가 있는 요소

 

 

✅ 이벤트 버블링 (bubbling phase)

 

이벤트가 하위 요소에서 상위 요소 방향으로 전파되는 단계이다. 위와 같은 코드에서 캡쳐링을 전달하는 true 부분만 지워주고 실행하니 3번을 클릭하면 3 → 2 → 1 순으로 이벤트가 발생하는 것을 확인할 수 있다.

 

이벤트 흐름은 캡쳐링, 버블링 어느 하나만 발생하는 것이 아니라 캡쳐링에서 시작하여 타깃에 도달하고 버블링으로 전파되어 종료된다. 위의 예제에서 캡쳐링과 버블링에 대한 이벤트를 모두 설정해 주면 더 명확히 확인할 수 있다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- 생략 -->
    <title>이벤트 버블링과 캡쳐링</title>
  </head>
  <body>
    <!-- 생략 -->
    </div>
    <script>
      // 버블링
      const divs = document.querySelectorAll("div");
      divs.forEach((div) => {
        div.addEventListener("click", (e) =>
          alert(e.currentTarget.getAttribute("id"))
        );
      });
      // 캡쳐링
      divs.forEach((div) => {
        div.addEventListener(
          "click",
          (e) => alert(e.currentTarget.getAttribute("id")),
          true
        );
      });
    </script>
  </body>
</html>

3번을 클릭하면  1 → 2 → 3 3→ 2 → 1 순으로 이벤트가 전파되므로 target(3번) 방향으로 전파되는 캡쳐링 단계와 타깃에 도달한 단계, 타깃에서 시작해서 상위 방향으로 전파되는 버블링 단계가 모두 있는 것을 확인할 수 있다.

정리하자면 이벤트는 이벤트를 발생시킨 target뿐만 아니라 상위 DOM 요소에서도 확인할 수 있다는 의미이다.

이벤트 위임

캡쳐링과 버블링을 활용하면 이벤트 핸들링 패턴인 이벤트 위임을 구현할 수 있다. 이벤트는 이벤트 타깃뿐만 아니라 상위 요소에서도 캐치할 수 있기 때문이다. 예를 들어, 게시물 목록의 글을 삭제 버튼을 클릭하여 삭제하고 싶을 때 일일이 모든 게시물에 삭제 이벤트 핸들러를 등록하는 것이 아닌, 전체 컨테이너에 하나의 이벤트만 등록하고 클릭했을 때 target을 검증 후 삭제를 해주면 되는 방식이다.

 

 

✅ 이벤트 위임 (event delegation)

 

이처럼 이벤트 위임은 여러 개의 하위 요소에 각각 이벤트 핸들러를 등록하는 대신 하나의 상위 요소에 이벤트 핸들러를 등록하는 방법을 말한다. 이벤트 위임은 버블링을 사용한 방식으로 컨테이너 내부 요소에 '클릭'이라는 이벤트가 발생하면 그 이벤트가 자식 요소에서 부모 요소로 버블링 되어 부모 요소에 달려 있는 이벤트 핸들러 함수를 실행시키게 되는 원리이다. 

 

주의할 점은 상위 요소에 이벤트 핸들러를 등록하기 때문에 이벤트를 실제로 발생시키는 이벤트 타깃(target)이 기대한 요소가 아닐 수도 있다는 점이다. 따라서 특정한 이벤트 타깃에 이벤트 핸들러를 동작하게 하고 싶다면 그 요소에 한정하여 타깃을 검증하는 절차가 필요하다.

 

 

🛠️ 로그인폼 focusout 이벤트 리팩토링 하기

위에서 공부한 개념을 적용해 볼 수 있는 상황을 고민하다가 마침 만들어 놓은 로그인 페이지 폼(form) 양식을 검증하는 코드에 버블링을 활용하여 이벤트 위임을 해볼 수 있지 않을까 생각되어 해당 코드를 리팩토링을 해보기로 했다.

 

현재 코드는 이메일과 비밀번호 input 창에서 각각 focusout 된다면 전달한 이벤트 핸들러 함수를 실행하게 된다. 

// 이메일, 비밀번호 input에서 focus out할 때, 값이 없을 경우 에러메세지
const checkEmailValue = (e) => {
  if (!e.target.value) {
    emailError.textContent = "이메일을 입력해주세요";
    addErrorClass(email);
    return;
  }
  ...
};

const checkPasswordValue = (e) => {
  if (!e.target.value) {
    passwordError.textContent = "비밀번호를 입력해주세요";
    addErrorClass(password);
    return;
  }
  ...
};

email.addEventListener("focusout", checkEmailValue); // email input 요소
password.addEventListener("focusout", checkPasswordValue); // password input 요소

 

이메일과 비밀번호 input 요소에 달려있는 이벤트를 제거하고 상위 요소인 form 요소에 focusout 이벤트가 일어났을 때 실행할 핸들러를 등록해 준다. 그러면 이메일과 비밀번호 input에서 focustout이 발생하면 상위요소로 이벤트가 버블링 되어 form에 등록한 함수가 실행된다. 참고로 focusout 이벤트와 발생 시점이 같은 이벤트 타입으로 blur가 있지만 blur는 버블링 되지 않는 특징이 있다.

// 이메일, 비밀번호 input에서 focus out할 때, 값이 없을 경우 에러메세지
const checkFormValue = (e) => {
  console.log(e.type); // focusout
  console.log(e.eventPhase); // 3
  console.log(e.target.value); // ㅇㅇㄹㅇㄴ
  console.log(e.currentTarget); // <form class="form" action="folder.html">...</form>

  if (!e.target.classList.contains("form__input")) return;

  const { value, id } = e.target;
  const context = id === "email" ? "이메일을" : "비밀번호를";

  if (!value) {
    document.getElementById(`${id}Error`).textContent = `${context} 입력해주세요`;
    addErrorClass(e.target);
    return;
  }
  ...
};

form.addEventListener("focusout", checkFormValue);

 

콘솔에 이벤트가 발생했을 때 target과 currentTarget을 출력해 보면 target은 foucusout 이벤트를 발생시킨 요소로 이메일 input 요소이고, currentTarget은 이벤트 핸들러가 바인딩된 요소로 form 요소가 출력된 것을 확인할 수 있다. 그러므로 if문을 통해서 이벤트를 발생시킨 target이 이벤트를 실행시키고 싶은 대상인지를 검증해 주는 추가 작업이 필요하다.

event 객체에 있는 eventPhase 프로퍼티는 이벤트 전파단계를 number로 알려준다. 출력된 3은 버블링 단계로 발생한 이벤트가 버블링 되어 전파 중인 것을 의미한다. (0️⃣ - 이벤트 없음, 1️⃣ - 캡쳐링 단계, 2️⃣ - 타깃 단계, 3️⃣ - 버블링 단계)

 

마무리

기능이 정상적으로 동작하는 것을 보니 뿌듯한 마음이 든다. 무엇보다 대충 알았던 개념을 확실히 알고 적용한 것 같아서 앞으로 이벤트를 어떻게 관리해야 하는지에 대한 감이 생긴 것 같다. 또 지금은 두 개의 요소에 바인딩된 이벤트 핸들러를 한 개로 줄인 단순한 작업이지만 향후 input 요소가 더 늘어나거나 동적으로 요소를 추가해야 하는 경우에도 일일이 이벤트 핸들러를 등록할 필요가 없기 때문에 여러모로 장점이 있다.

 

처음 이벤트 버블링과 캡쳐링 개념을 공부했을 때 글로는 이해가 되었지만 "그래서 버블링과 캡쳐링을 알아두면 왜 유용한지"에 대한 의문이 계속 들었었다. 그 의문 때문인지 온전히 이해했다는 느낌이 들지 않았었는데 글로 정리하면서 확실히 깨달았다는 생각이 들었다. 결국 이벤트 전달 방식을 잘 이해한다면 이벤트를 설계할 때 이벤트 등록을 최소화하여 불필요한 이벤트 등록을 줄일 수 있고, 이는 곧 성능과 유지보수와도 직결되기 때문이다.

 


참고

모던자바스크립트 딥다이브 - 이벤트

https://choonse.com/2022/01/14/651/