본문 바로가기

JavaScript/모던 자바스크립트 튜토리얼

커스텀 이벤트 디스패치

https://ko.javascript.info/dispatch-events

 

커스텀 이벤트 디스패치

 

ko.javascript.info

 

커스텀 이벤트는 그래픽 컴포넌트를 만들 때 사용됩니다. 자바스크립트 기반 메뉴가 있다고 가정해봅시다. 개발자는 메뉴의 루트 요소에 open(메뉴를 열었을 때 실행됨), select(항목을 선택했을 때 실행됨)같은 이벤트를 달아 상황에 맞게 이벤트가 실행되게 할 수 있습니다. 이렇게 루트 요소에 이벤트 핸들러를 달아놓으면 바깥 코드에서도 이벤트 리스닝을 통해 메뉴에서 어떤 일이  일어났는지를 파악할 수 있습니다.

 

자바스크립트를 사용하면 새로운 커스텀 이벤트 뿐만 아니라 목적에 따라 click, mousedown 같은 내장 이벤트를 직접 만들 수도 있습니다. 이렇게 만든 내장 이벤트들은 테스팅을 자동화할 때 유용합니다. 

Event의 생성자

내장  이벤트 클래스는 DOM 요소 클래스 같이 계층 구조를 형성합니다. 내장 이벤트 클래스 계층의 꼭대기엔 Event 클래스가 있습니다. Event 객체는 다음과 같이 생성할 수 있습니다.

 

let event = new Event(type[, options]);

 

인수는 다음과 같습니다.

- type: 이벤트 타입을 나타내는 문자열로 "click" 같은 내장 이벤트, "my-event" 같은 커스텀 이벤트가 올 수도 있습니다. 

- options: 두 개의 선택 프로퍼티가 있는 객체가 옵니다. 

  - bubbles: true / false (true)인 경우 이벤트가 버블링

  - cancelable: true / false (true)인 경우 브라우저 '기본 동작'이 실행되지 않습니다 .

 

아무런 값도 지정하지 않으면 두 프로퍼티는 기본적으로 {bubbles: false, cancelable: false} 처럼 false가 됩니다. 

dispatchEvent

이벤트 객체를 생성한 다음엔 elem.dispatchEvent(event)를 호출해 요소에 있는 이벤트를 반드시 '실행'시켜줘야 합니다. 이렇게 이벤트를 실행시켜줘야 핸들러가 일반 브라우저 이벤트처럼 이벤트에 반응할 수 있습니다. bubbles 플래그를 true로 해서 이벤트를 만든 경우 이벤트는 제대로 버블링됩니다. 예시를 봅시다. 자바스크립트를 사용해 click 이벤트를 만들고 실행 시켜 보았습니다. 버튼은 실제로 클릭하지 않았지만, 이벤트 핸들러가 동작하는 것을 확인할 수 있습니다. 

 

<button id="elem" onclick="alert('클릭!');">자동으로 클릭 되는 버튼</button>

<script>
  let event = new Event("click");
  elem.dispatchEvent(event);
</script>

 

event.isTrusted

event.isTrusted를 사용하면 이벤트가 스크립트를 통해 생성한 이벤트인지 '진짜' 사용자가 만든 이벤트인지 알 수 있습니다. event의 isTrusted 프로퍼티가 true이면 사용자 액션을 통해 만든 이벤트라는 것을 의미합니다. isTrusted가 false이면 해당 이벤트가 스크립트를 통해 생성되었다는 걸 알 수 있습니다. 

커스텀 이벤트 버블링 예시

"hello"라는 이름을 가진 이벤트를 만들고 버블링 시켜서 document에서 이벤트를 처리할 수 있게 해보겠습니다. 

이벤트가 버블링되게 하려면 bubbles를 true로 설정해야 합니다.

<h1 id="elem">Hello from the script!</h1>

<script>
  // 버블링이 일어나면서 document에서 이벤트가 처리됨
  document.addEventListener("hello", function(event) { // (1)
    alert("Hello from " + event.target.tagName); // Hello from H1
  });

  // 이벤트(hello)를 만들고 elem에서 이벤트 디스패치
  let event = new Event("hello", {bubbles: true}); // (2)
  elem.dispatchEvent(event);

  // document에 할당된 핸들러가 동작하고 메시지가 얼럿창에 출력됩니다.

</script>

위 예시에서 주의해서 볼 점은 다음과 같습니다. 

1. on<event>은 내장 이벤트에만 해당하는 문법이기 때문에 document.onhello라고 하면 원하는 대로 동작하지 않습니다. 커스텀 이벤트는 반드시 addEventListener을 사용해 핸들링해야 합니다. 

2. bubbles: true를 명시적으로 설정하지 않으면 이벤트가 버블링되지 않습니다. 

 

내장 이벤트(click)와 커스텀 이벤트(hello)의 버블링 메커니즘은 동일합니다. 이에 더하여 커스텀 이벤트에도 내장 이벤트와 마찬가지로 캡쳐링, 버블링 단계가 있습니다. 

MouseEvent, KeyboardEvent등의 다양한 이벤트

명세서의 UI 이벤트 섹션에 다양한 UI 이벤트 클래스가 명시되어 있습니다. 그중 일부를 추리면 다음과 같습니다.

- UIEvent

- FocusEvent

- MouseEvent

- WheelEvent

- KeyboardEvent

 

그런데 이 이벤트들은 new Event로 만들면 안되고, 반드시 관련 내장 클래스를 사용해야 합니다. 마우스 클릭 이벤트라면 new MouseEvent("click")를 사용해야 하죠. 이렇게 제대로 된 생성자를 사용해야만 해당 이벤트 전용 표준 프로퍼티를 명시할 수 있습니다. 

 

new MouseEvent("click")를 사용해 마우스 이벤트의 clientX, clientY 프로퍼티를 설정해 보겠습니다.

 

let event = new MouseEvent("click", {
  bubbles: true,
  cancelable: true,
  clientX: 100,
  clientY: 100
});

alert(event.clientX); // 100

 

이제 일반 Event 생성자를 사용해 표준 프로퍼티를 설정해 보겠습니다.  제대로 동작하지 않는다는 것을 직접 확인할 수 있습니다.

 

let event = new Event("click", {
  bubbles: true, // Event 생성자에선
  cancelable: true, // bubbles와 cancelable 프로퍼티만 동작합니다.
  clientX: 100,
  clientY: 100
});

alert(event.clientX); // undefined, 알 수 없는 프로퍼티이기 때문에 무시됩니다.

 

new Event로 이벤트를 생성한 다음, event.clientX = 100 처럼 프로퍼티에 값을 직접 명시해주면 이런 제약을 피할 수 있긴 합니다. 위와 같은 제약을 따르는 건 개발자 마음이긴 하죠. 그렇지만 브라우저에서 만들어지는 UI 이벤트는 적확한 이벤트 타입이 있다는 것을 알아두는게 좋습니다. UI 이벤트별 표준 프로퍼티 목록은 명세서에서 확인할 수 있습니다.

커스텀 이벤트

지금까진 new Event로 커스텀 이벤트를 만들었습니다. 하지만 제대로 된 커스텀 이벤트를 만들려면 new CustomEvent를 사용해야 합니다. CustomEvent는 Event와 거의 유사하지만 한 가지 다른 점이 있습니다. CustomEvent의 두 번째 인수엔 객체가 들어갈 수 있는데, 개발자는 이 객체에 detail이라는 프로퍼티를 추가해 커스텀 이벤트 관련 정보를 명시하고, 정보를 이벤트에 전달할 수 있다.

 

<h1 id="elem">이보라님, 환영합니다!</h1>

<script>
  // 추가 정보는 이벤트와 함께 핸들러에 전달됩니다.
  elem.addEventListener("hello", function(event) {
    alert(event.detail.name);
  });

  elem.dispatchEvent(new CustomEvent("hello", {
    detail: { name: "보라" }
  }));
</script>

detail 프로퍼티엔 어떤 데이터도 들어갈 수 있습니다. 사실 new Event로 일반 이벤트를 생성한 다음 추가 정보가 담긴 프로퍼티를 이벤트 객체에 추가해주면 되기 때문에 detail 프로퍼티 없이도 충분히 이벤트에 원하는 정보를 추가할 수 있긴 합니다. 그런데도 detail이라는 특별한 프로퍼티를 사용하는 이유는 다른 이벤트 프로퍼티와 충돌을 피하기 위해서 입니다. 이외에도 new CustomEvent를 사용하면 코드 자체만으로 '커스텀 이벤트'라고 설명해주는 효과가 있습니다.

 

event.preventDefault()

브라우저 이벤트 대다수는 '기본 동작'과 함께 실행됩니다. 링크 클릭시 특정 URL로 이동하기, 전송 버튼 클릭시 서버에 폼 전송하기 같은 동작은 이런 기본 동작의 대표적인 예입니다. 우리가 직접 만든 커스텀 이벤트는 당연히 기본 동작이 없습니다. 하지만 커스텀 이벤트에도 기본 동작을 설정해줄 수 있습니다. 이벤트 기본 동작은 event.preventDefault()를 호출해 취소할 수 있습니다. event.preventDefault()를 호출하면 이벤트 핸들러는 기본 동작이 취소되어야 한다는 신호를 보내기 때문입니다. 이벤트 기본 동작이 취소되면 elem.dispatchEvent(event) 호출 시 false가 반환됩니다. 해당 이벤트를 디스패치하는 코드에선 이를 통해 기본동작이 취소되어야 한다는 것을 인지합니다.

이벤트 안 이벤트

이벤트는 대게 큐에서 처리됩니다.  따라서 브라우저가 onclick 이벤트를 처리하고 있는데 마우스를 움직여서 새로운 이벤트를 발생시키면 이 이벤트에 상응하는 mousemove 핸들러는 onclick 이벤트 처리가 끝난 후에 호출됩니다.  그런데 이벤트 안 dispatchEvent 처럼 안에 다른 이벤트가 있는 경우엔 위와 같은 규칙이 적용되지 않습니다. 이벤트 안에 있는 이벤트는 즉시 처리됩니다. 새로운 이벤트 핸들러가 호출되고 난 후에 현재 이벤트 핸들링이 재개됩니다. 

 

<button id="menu">메뉴(클릭해주세요)</button>

<script>
  menu.onclick = function() {
    alert(1);

    menu.dispatchEvent(new CustomEvent("menu-open", {
      bubbles: true
    }));

    alert(2);
  };

  // 1과 2 사이에 트리거됩니다
  document.addEventListener('menu-open', () => alert('중첩 이벤트'));
</script>

 

얼럿 창에 1, 중첩 이벤트, 2가 차례대로 출력되는 것을 확인할 수 있습니다. 이 예시에서 주목해야 할 것은 중첩  이벤트 menu-open이 document에 할당된 핸들러에서 처리된다는 점입니다. 중첩 이벤트의 전파와 핸들링이 외부 코드(onclick)의 처리가 다시 시작되기 전에 끝났습니다. 이런 일은 중첩 이벤트가 dispatchEvent일 때뿐만 아니라 이벤트 핸들러 안에서 다른 이벤트를 트리거하는 메서드를 호출할 때 발생합니다.  즉, 이벤트 안 이벤트는 동기적으로 처리되는 것이죠. 그런데 때에 따라서 중첩 이벤트가 동기적으로 처리되는걸 원치 않는 경우도 있습니다. 위 예시에서 menu-open이벤트나 다른 이벤트의 처리 여부와 관계없이 onclick 이벤트를 먼저 처리하려면 어떻게 해야할까요?

onclick 끝에 dispatchEvent 등의 이벤트 트리거 호출을 넣는게 하나의 방법이 될 수 있습니다. 이에 더하여 중첩 이벤트를 지연 시간이 0인 setTimeout으로 감싸는 것도 방법입니다.

 

<button id="menu">Menu (click me)</button>

<script>
  menu.onclick = function() {
    alert(1);

    setTimeout(() => menu.dispatchEvent(new CustomEvent("menu-open", {
      bubbles: true
    })));

    alert(2);
  };

  document.addEventListener('menu-open', () => alert('중첩 이벤트'));
</script>

 

이제 원하는 대로 dispatchEvent가 mouse.onclick을 포함한 현재 코드 실행이 종료된 이후에 질행됩니다. 

1 -> 2 -> 중첩이벤트

'JavaScript > 모던 자바스크립트 튜토리얼' 카테고리의 다른 글

문서 수정하기  (0) 2023.03.24
call/apply와 데코레이터, 포워딩  (0) 2023.03.22
DOM 탐색하기  (0) 2023.03.16
getElement*, querySelector*로 요소 검색하기  (0) 2023.03.16
iterable 객체  (0) 2023.03.16