#event, #web

Web Event 다루기

Web Event 다루기

웹 개발에서 프론트엔드 영역이 이토록 발전할 수 있었던 가장 주된 이유는 웹의 특성이 단순한 ‘페이지’에서 ‘애플리케이션’으로 진화했다는 사실일 겁니다. 그리고 이 말은 곧, 좋은 웹의 기준을 판단할 때 사용자의 행동에 따라 웹이 얼마나 자연스럽게 반응할 수 있는지가 중요함을 나타냅니다.

자연스러운 인터랙션을 위해서는 UI, UX, 성능 등 다양한 요소를 고려해야겠지만 무엇보다 인터랙션 과정의 시작을 알리는 이벤트를 빼놓지 않을 수 없습니다. 그럼 지금부터 이벤트에 대해 함께 알아볼까요?

Event

이벤트는 말 그대로 행동 혹은 사건입니다. 이것을 프로그래밍 맥락에서 유추해보면, 코드 내에서 발생하는 행동이라고 볼 수 있겠습니다. 웹을 사용하면서 클릭을 하거나 키보드를 입력해야 할 때, 더 나아가 모바일 환경에서 터치할 때 일어나는 모든 일들은 각기 다른 이벤트입니다. 이제 이벤트에 대한 개념은 얼추 파악을 마쳤으니 빠르게 이벤트를 다루는 방법으로 넘어가겠습니다.

Event Listener

이벤트를 코드로 다룰 때는 특정 요소에 이벤트를 등록하는 과정을 거칩니다. 이 과정을 좀 더 쉽게 풀어보면, “사용자가 a 행동을 하는 순간에 그에 맞는 브라우저의 행동을 보여줘”라고 할 수 있겠네요. 브라우저의 행동은 우리가 흔히 작성하는 함수를 예상할 수 있습니다. 하지만 사용자가 무슨 행동을 할지, 이벤트를 무슨 요소에 등록할지는 어떻게 알려줄 수 있을까요?

이러한 요구를 충족하기 위해 출현한 개념이 바로 이벤트 리스너입니다. 무언가를 리스닝(listening)한다는 것은 쉽게 말해 대기를 하고 있다고 이해하는 것이 편합니다. 개발자는 이벤트 리스너를 통해 브라우저 내의 특정 요소특정 행동을 등록해놓고 사용자가 특정 행동을 할 때까지 기다리도록 명령할 수 있습니다.

이벤트를 등록하는 방법은

  • HTML 태그에 직접 등록하는 Inline event
  • JS에서 DOM을 가져와 등록하는 Event Handler (e.g. button.onClick)
  • JS에서 DOM을 가져와 등록하는 EventTarget.addEventListener

위와 같이 크게 세 가지로 나눌 수 있습니다. 이번 글에서는 마지막 방법인 EventTarget.addEventListener를 예시로 들어 설명하겠습니다.

<body>
  <button id="button">Click me</button>
</body>
<script>
  var button = document.getElementById('#button');

  button.addEventListener('click', function () {
    console.log('버튼이 클릭되었음');
  });
</script>

예시로 작성한 코드는 다음과 같은 흐름으로 실행됩니다.

  1. #button이라는 id를 가진 HTML의 요소를 가져와서
  2. addEventListener를 선언해 이벤트를 대기할 것이라고 알려준다.
  3. 이벤트의 타입은 'click'이 될 것이고
  4. 해당 이벤트가 발생하면 그에 따른 브라우저 동작은 두 번째 인자로 들어간 함수의 실행이다.

두 번째 인자로 들어간 함수는 이벤트 리스너의 관점에서 이벤트 핸들러가 됩니다. 용어만 달라졌을 뿐 함수라는 사실은 변함이 없는데요, 단지 이벤트로 일어날 행동을 정의한 함수를 의미합니다.

한가지 염두에 두어야 할 점은, addEventListenerWeb API (Web에서 제공하는 인터페이스 e.g. fetch, setTimeout etc.) 로 취급된다는 것입니다. 스크립트 파일 내에서 JS 문법과 같이 작성하기 때문에 착각하기 쉽지만, 이 부분을 혼동하는 일은 없길 바랍니다.

Event Object

이벤트 핸들러로 정의되어 호출된 함수는 하나의 인자를 받는 것이 원칙입니다. 여기서 넘어간 인자의 정체는 바로 이벤트 객체입니다. 이벤트 객체는 발생한 이벤트에 대한 전반적인 정보를 담고 있는 객체입니다. 대부분은 e, event라는 이름으로 선언되지만 사실 아무 이름이나 들어가도 상관은 없습니다.

var button = document.getElementById('#button');

button.addEventListener('click', function (event) {
  console.log(event); // PointerEvent { ... }
});

이 객체 안을 들여다보면 이벤트가 등록된 요소나 타겟 요소, 현재 좌표 등을 알 수 있으므로 이를 통해 더욱 세밀한 이벤트의 작성이 가능해집니다.

그건 그렇고 여기서 하나의 의문점을 짚고 넘어가야 할 것 같습니다. event라는 인자를 선언만 해주었지, 어디서도 이벤트 객체를 넘겨주겠다는 코드를 작성하진 않은 것 같은데 이것은 당최 어떻게 이벤트 핸들러로 넘어가는 걸까요? 이 메커니즘을 이해하기 위해서는 이벤트 객체가 어디서부터 출발하여 전달되는지 알아야 합니다.

Event Flow

하나의 웹 페이지는 Window를 최상위로 가진 계층적인 구조를 생성합니다. 이벤트 객체는 이 구조와 이벤트의 Phase에 따라 결정된 전파 경로(Propagation path)를 기준으로 흘러가게 됩니다.

이벤트의 Phase는 총 3단계로 나누어볼 수 있습니다.

  • Capture Phase: 이벤트 객체가 Window로부터 시작하여 타겟의 부모 요소에 도달하는 단계
  • Target Phase: 이벤트 객체가 타겟에 도달하는 단계.
  • Bubbling Phase: Capture Phase의 역순. 타겟의 부모 요소부터 출발해 Window 요소에 도달하는 단계

이벤트 객체의 전파 경로 상에서 등록된 이벤트 핸들러가 있다면, 해당 이벤트 핸들러는 호출되고 이벤트 객체를 인자로 받게 됩니다. 그것이 바로 모든 이벤트 핸들러에서 이벤트 객체를 확인할 수 있는 이유입니다.

현대의 브라우저들은 Bubbling Phase를 기본 동작으로 설정해놓았다고 하네요. 특수한 경우에서 Capture Phase를 사용해야 한다면, EventTarget.addEventListener의 세 번째 인자인 useCapturetrue로 설정해주면 됩니다.

In modern browsers, by default, all event handlers are registered for the bubbling phase. - MDN

currentTarget, target

앞서 설명한 이벤트 객체에는 이벤트와 관련된 요소에 접근할 수 있는 프로퍼티를 제공합니다.

  • currentTarget: 이벤트 리스너를 등록한 요소. 이벤트 핸들러의 this.
  • target: 이벤트 전파 경로의 최하위 요소.

이 두 프로퍼티가 항상 같은 요소를 가리키고 있다고는 보장할 수 없습니다. 이벤트를 등록한 요소에 자식 요소가 존재하는 경우가 그렇습니다.

<body>
  <ul id="parent">
    <li id="child">child</li>
  </ul>
</body>
<script>
  const parent = document.getElementById('parent');

  parent.addEventListener('click', function (event) {
    console.log(event.currentTarget); // <ul id="parent"></ul>
    console.log(event.target); // <li id="child">child</li>
  });
</script>

초반에는 조금 헷갈리겠지만, 이벤트의 흐름과 앞서 말한 두 타겟의 차이점을 잘 알아두면 이벤트 위임과 같은 숙련된 테크닉을 사용할 수도 있습니다.

References