ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 브라우저가 이벤트를 처리하는 방법
    웹 브라우저 2023. 3. 17. 13:46

    웹 페이지를 (특히 동적 웹 페이지) 만들 때 이벤트는 매우 중요한 요소이다.

    단순히 내 첫 팀프로젝트로 만든 웹 페이지만 해도 처리할 이벤트가 굉장히 많았다.

    이런 식으로 프로그램의 흐름을 이벤트 중심으로 제어하는 방식을 event-driven programming 이라고 한다는데,

    그게 중요한 건 아니고, 나는 수많은 이벤트 핸들러를 만들면서 이 이벤트들이 정확히 어떻게 동작하는 지 생각해보지 않았다.

    여태까지는 문제가 없었지만, 내가 이벤트 핸들러를 만들었을 때 이게 어디로 가서 어떻게 동작하는 지 원리를 알고 있으면 확실히 문제 해결능력에 도움이 될 것이다.

     


    출처 : 모던 자바스크립트 Deep Dive: 자바스크립트의 기본 개념과 동작 원리 / 이웅모 지음

     

     

    이벤트 드리븐 프로그래밍

     

    이벤트가 발생하는 방식은 다음과 같다.

    리액트 코드를 짤 때, 어느 버튼에 마우스 클릭을 했을 때 페이지의 요소가 바뀌는 이벤트를 만들고 싶다고 하자.

    그럼 우선 이벤트 핸들러 함수를 만든다.

     그리고 jsx 를 작성할 때, onClick 어트리뷰트에 그 이벤트 핸들러를 등록한다.

    ...
    function handleButtonClick() {
    	console.log("Clicked");
    }
    ...
    <button type="button" onClick={handleButtonClick}>click!</button>
    ...

    이 행위가 정확히 어떤 것이냐면,

    일단 이벤트 핸들러 함수를 만든 뒤,

    이벤트 핸들러를 등록함으로써 브라우저에게 함수를 호출할 권한을 위임하는 것이다 !

    왜냐면, 코드를 작성할 때 나는 이 이벤트 핸들러가 언제 호출될 지 알 수 없지만,

    브라우저는 사용자의 버튼 클릭을 감지해서 언제 이벤트를 발생시킬 지 알 수 있기 때문.

     

    그래서 브라우저에서 개발자 도구를 켜고 element 섹션의 Event Listeners 탭을 보면,

    브라우저에 수많은 이벤트 핸들러들이 등록되어 있는 것을 확인할 수 있다.

    그리고 그 중 하나를 눌러보면, 소스코드에서 해당 이벤트 핸들러가 작성된 라인을 볼 수 있다.

     

    그래서 이벤트 핸들러를 등록할 때 콜백함수와 마찬가지로 함수의 참조를 등록하는 것 !

    // 함수의 호출문을 등록하면 호출문의 평가 결과가 등록되어 버린다 ..
    <button type="button" onClick={handleButtonClick()}>click!</button>
    
    // Correct ! 함수 참조를 등록함으로써 이벤트 핸들러를 올바르게 브라우저에게 위임한다.
    <button type="button" onClick={handleButtonClick}>click!</button>

    사실 리액트 인강을 들을 때 아주 짧게 설명하고 넘어갔던 것이 생각이 난다.

    그 때는 그냥 그런갑다 하고 넘어갔지만, 역시 복기를 하니까 도움이 된다 !

     


     

    이벤트 객체

     

     

    이벤트 핸들러를 만들 때 자주 사용하는 인자로 다음과 같은 경우가 있다.

    ...
    function handleButtonClick(e) {
    	console.log(e.clientX, e.clientY);
    }
    ...
    <button type="button" onClick={handleButtonClick}>click!</button>
    ...

    이 때 눈여겨볼 점은, 이벤트 핸들러를 등록할 때 어떠한 인자도 전달해주지 않았다는 것.

    그렇다면 이벤트 핸들러가 받는 e의 정체는 뭘까 ?

    바로, 이벤트가 발생하는 시점에 "이벤트 객체" 라는 것이 동적으로 생성되는데, 생성된 이벤트 객체는 이벤트 핸들러의 첫 번째 인수로 전달되는 것 !

     

    매개변수의 이름이 e, 또는 event 등 아무런 이름이어도 상관 없고, 단순히 이벤트 핸들러의 첫 번째 인수에 암묵적으로 할당된다. 하지만 가독성을 위해 e 혹은 event 를 쓰는 것이 좋다. (이벤트 핸들러 어트리뷰트 방식일 경우 event 로만 이벤트 객체를 전달받을 수 있다, 왜냐면 이벤트 핸들러 어트리뷰트 값은 사실 암묵적으로 이벤트 핸들러 함수를 만들고, 그 내부에 내가 이벤트 핸들러로 선언한 함수를 호출하는데 이 때 event 라는 이름의 인자를 씀)

     

     

    타입스크립트로 원활히 코딩을 하려면 이벤트 객체의 타입을 꼭 알아둬야 한다.

    fig 1. 이벤트 객체의 상속 구조. (출처: 모던 자바스크립트 Deep Dive)

     

    이벤트 객체의 공통 프로퍼티 중 eventPhase 라는 것이 있는데, 이것을 이해하기 위해선 DOM 요소 노드에서 발생한 이벤트가 어떻게 DOM 트리를 통해 전파되는 지 이해해야 한다.

     

     


     

    이벤트 전파

     

     

    이벤트 핸들러를 만들고, 특정 HTML 요소에 이벤트 핸들러를 등록했다. 이것이 DOM 트리로 파싱되어 브라우저에 전달되고, 브라우저에서 그 이벤트의 발생을 감지해서, 이벤트 객체가 생성되었다. 그 다음엔 어떤 일이 일어나는가 ?

     

    우선 이벤트가 발생한 DOM 요소는 이벤트 타깃 (event target) 이 된다.

    그리고 이벤트 객체는 전역 객체인 window 에서 시작해서, 이벤트 타깃까지 DOM 트리의 하위 요소 방향으로 전파된다.

    이를 캡처링 단계 (capturing phase) 라고 한다.

    이후 이벤트 객체가 이벤트 타깃에 도달하면, 이것을 타깃 단계 (target phase) 라고 한다.

    이후 이벤트 객체는 다시 이벤트 타깃에서 시작해서 window 방향으로 전파되는데, 이를 버블링 단계 (bubbling phase) 라고 한다.

     

    요약하면, 이벤트 객체는 window 에서 이벤트 타깃을 중심으로 전파되는데, 3단계를 거쳐서 이벤트 타깃까지 도달했다가 다시 window 까지 전파된다. 이 3단계는 다음과 같다.

    캡처링 단계 (capturing phase) -> 타깃 단계 (target phase) -> 버블링 단계 (bubbling phase)

     

    이것이 무엇을 의미하는가 ?

    바로, 이벤트는 이벤트 타깃의 상위 DOM 요소에서도 캐치할 수 있다는 것을 의미한다 !

    이를 통해 많은 것을 할 수 있다. (동적으로 하위 DOM 요소를 추가할 때 일일이 이벤트 핸들러를 등록하는 대신, 하나의 상위 DOM 요소에 이벤트 핸들러를 등록한다든지 하는)

    이벤트 위임에 대해 알아보기 전에, 먼저 짚고 넘어갈 게 있다.

     

    버블링을 통해 전파되지 않는 이벤트가 있다.

    • 포커스 이벤트: focus/blur
    • 리소스 이벤트: load/unload/abort/error
    • 마우스 이벤트: mouseenter/mouseleave

     

    위 이벤트들은 상위 요소에서 캐치해야 할 경우가 그리 많지 않다. 보통은 캡처링 단계의 이벤트를 캐치하는 일은 하지 않으므로, 위 이벤트를 상위 요소에서 캐치해야 할 경우 대체할 수 있는 이벤트가 존재한다.

    • focus/blur -> focusin/focusout
    • mouseenter/mouseleave -> mouseover/mouseout

     


     

    이벤트 핸들러에 인수 전달

     

    이것은 짧고 간단하지만 매우 중요하다 !

    이벤트 핸들러를 등록할 때는 함수 호출문을 쓸 수 없고, 함수 참조를 등록해야 하기 때문에 인수를 등록할 수 없다.

    하지만 이벤트 핸들러에 인자가 필요한 경우는 매우 많다 ! (내가 리액트로 코딩을 시작하고 가장 먼저 찾아본 것이 이게 아닐까 싶다)

     

    그러면 이벤트 핸들러에 인수를 전달하려면 어떻게 해야 할까 ?

    // case 1
    ...
    function handleClick(param) {
    	return () => {
        	// Event handler code using param
        }
    }
    ...
    ...
    <button type="button" onClick={handleClick(param)}>click!</button>
    ...
    
    // case 2
    ...
    function handleClick(param) {
    	// Event handler code using param
    }
    ...
    ...
    <button type="button" onClick={() => handleClick(param)}>click!</button>
    ...

    자세히 보면 알겠지만 위의 두 케이스는 사실,

    이벤트 핸들러를 리턴하는 함수를 하나 만들어서 그 함수 호출문을 등록하는 동일한 방법이다.

    따라서 이벤트 핸들러를 리턴하는 함수의 호출문을 등록한다 라는 규칙만 지키면, 위의 두 가지 케이스 말고도 다양한 방식이 나올 수 있다.

    본인이 보기에 가독성이 좋은 방식을 사용하거나, 팀의 컨벤션에 맞춰서 쓰면 되는 셈이다.

     

     


     

    커스텀 이벤트 디스패치

     

     

    내가 리액트로 첫 습작을 만들 때, 굉장히 애를 먹었던 부분이 있다.

    바로, 내가 "시작" 버튼을 누르면, 브라우저가 알아서 A, B, C, D 버튼을 순서대로 누르게 할 수 없을까 ? 하는 고민이었다.

    그리고 실제로 그럴 수 있는 방법이 있었다 !!

    우선 A, B, C, D 버튼을 누르는 커스텀 이벤트를 만들고,

    "시작"버튼을 누르면 dispatchEvent 메서드를 통해 그 커스텀 이벤트를 실행시키면 된다 !

    import { useRef } from "react";
    ...
    ...
    // 커스텀 이벤트 생성
    const mouseClickEvent = new MouseEvent('click', {
    	bubbles: true, // default: false
    	canelable: true, // default: false
    });
    
    function handleClickAButton() {
    	console.log("A clicked");
    }
    
    function handleClick() {
    	if (buttonRef.current)
    		buttonRef.current.dispatchEvent(mouseClickEvent);
    }
    ...
    ...
    <button type="button" ref={buttonRef} onClick={handleClickAButton}>A</button>
    <button type="button" onClick={handleClick}>click!</button>
    ...

    주의할 점 : dispatchEvent 메서드는 이벤트 핸들러를 동기 처리 방식으로 호출한다. 즉, dispatch 메서드를 사용하기 전에 커스텀 이벤트를 처리할 이벤트 핸들러를 등록해야 한다.

     

     

    이 방법을 사용하면 이벤트를 보다 유연하게 사용할 수 있을 것 같다.

    미리 알아뒀으면 습작을 할 때 더 쉽게 만들 수 있었을텐데 ..

    댓글

Designed by Tistory.