5.24 Javascript Event

사용자와 웹페이지의 상호작용을 위한 이벤트

1. Introduction

이벤트(event)는 어떤 사건을 의미한다. 브라우저에서의 이벤트란 예를 들어 사용자가 버튼을 클릭했을 때, 웹페이지가 로드되었을 때와 같은 것인데 이것은 DOM 요소와 관련이 있다.

이벤트가 발생하는 시점이나 순서를 사전에 인지할 수 없으므로 일반적인 제어 흐름과는 다른 접근 방식이 필요하다. 즉 이벤트가 발생하면 누군가 이를 감지할 수 있어야 하며 그에 대응하는 처리를 호출해 주어야 한다.

브라우저는 이벤트를 감지할 수 있으며 이벤트 발생 시에는 통지해 준다. 이 과정을 통해 사용자와 웹페이지는 상호작용(Interaction)이 가능하게 된다.

interaction

이벤트가 발생하면 그에 맞는 반응을 하여야 한다. 이를 위해 이벤트는 일반적으로 함수에 연결되며 그 함수는 이벤트가 발생하기 전에는 실행되지 않다가 이벤트가 발생되면 실행된다. 이 함수를 이벤트 핸들러라 하며 이벤트에 대응하는 처리를 기술한다.

일반적으로 브라우저는 단일 쓰레드(single-thread)에서 이벤트 드리븐(event-driven) 방식으로 동작한다. 대부분의 이벤트 처리는 비동기적으로 수행되는데 이때 생성된 비동기식 이벤트는 이벤트 큐(queue)에 보관된다(동기식 이벤트는 큐에 들어가지 않는다). 큐에 보관되어 있는 이벤트는 이벤트 루프(Event Loop)에 의해 특정 시점에 수행된다.

event-loop

2. 이벤트의 종류

대표적인 이벤트를 소개한다. 자세한 사항은 Event reference을 참조 바란다.

2.1 UI Event

Event Description
load 웹페이지의 로드가 완료되었을 때
unload 웹페이지가 언로드될 때(주로 새로운 페이지를 요청한 경우)
error 브라우저가 자바스크립트 오류를 만났거나 요청한 자원이 존재하지 않는 경우
resize 브라우저 창의 크기를 조절했을 때
scroll 사용자가 페이지를 위아래로 스크롤할 때
select 텍스트를 선택했을 때

2.2 Keyboard Event

Event Description
keydown 키를 누르고 있을 때
keyup 누르고 있던 키를 뗄 때
keypress 키를 누르고 뗏을 때

2.3 Mouse Event

Event Description
click 마우스 버튼을 클릭했을 때
dbclick 마우스 버튼을 더블 클릭했을 때
mousedown 마우스 버튼을 누르고 있을 때
mouseup 누르고 있던 마우스 버튼을 뗄 때
mousemove 마우스를 움직일 때 (터치스크린에서 동작하지 않는다)
mouseover 마우스를 요소 위로 움직였를 때 (터치스크린에서 동작하지 않는다)
mouseout 마우스를 요소 밖으로 움직였를 때 (터치스크린에서 동작하지 않는다)

2.4 Focus Event

Event Description
focus/focusin 요소가 포커스를 얻었을 때
blur/foucusout 요소가 포커스를 잃었을 때

2.5 Form Event

Event Description
input input 또는 textarea 요소의 값이 변경되었을 때
  contenteditable 속성을 가진 요소의 값이 변경되었을 때
change select box, checkbox, radio button의 상태가 변경되었을 때
submit form을 submit할 때 (버튼 또는 키)
reset reset 버튼을 클릭할 때 (최근에는 사용 안함)

2.6 Clipboard Event

Event Description
cut 컨텐츠를 잘라내기할 때
copy 컨텐츠를 복사할 때
paste 컨텐츠를 붙여넣기할 때

3. Event Binding

3.1 HTML Event Handler

HTML 요소의 이벤트 속성에 이벤트 핸들러를 대응시키는 방법이다.

<!DOCTYPE html>
<html>
<body>
  <button onclick="myFunction()">Click me</button>
  <script>
    function myFunction() {
      alert('Button clicked!');
    }
  </script>
</body>
</html>

이 방식은 더 이상 사용되지 않으며 사용해서도 않된다. 오래된 코드에서 간혹 이 방식을 사용한 것이 있기 때문에 알아둘 필요는 있다. HTML과 Javascript는 분리되어야 한다.

3.2 전통적(Traditional) DOM Event Handler

HTML Event Handler처럼 HTML과 Javascript가 혼용되는 문제는 해결되었으나 이벤트 핸들러에 하나의 함수만을 바인딩할 수 있으며 함수에 인수를 전달할 수 없는 단점이 있다.

<!DOCTYPE html>
<html>
<body>
  <button id='btn'>Click me</button>
  <script>
    var btn = document.getElementById('btn');
    btn.onclick = function() {
      alert('Button clicked!');
    }
  </script>
</body>
</html>

3.3 DOM Level 2 Event Listener

Event Listener는 이벤트를 처리하는 가장 최신의 기법이다. addEventListener 함수를 이용하여 대상 요소에 이벤트를 바인딩하고 해당 이벤트가 발생했을 때 실행될 콜백 함수를 지정한다.

Event Listener

<!DOCTYPE html>
<html>
<body>
  <script>
    addEventListener('click', function() {
      console.log('Clicked!');
    });
  </script>
</body>
</html>

위와 같이 대상요소를 지정하지 않으면 click 이벤트는 전역객체 window에 바인딩된다.

<!DOCTYPE html>
<html>
<body>
  <label for='username'>User name </label>
  <input type='text' id='username'>
  <script>
    var elem = document.getElementById('username');
    elem.addEventListener('blur', function() {
      console.log('blur event occurred!');
    });
  </script>
</body>
</html>

input 요소를 blur 이벤트에 바인딩하였다. 사용자 이름이 최소 2자 이상이야한다는 규칙을 세우고 이에 부합하는지 확인하는 처리를 한다.

<!DOCTYPE html>
<html>
<body>
  <label for="username">User name </label>
  <input type='text' id='username'>
  <em id='message'></em>
  <script>
    var elem = document.getElementById('username');
    var msg  = document.getElementById('message');

    elem.addEventListener('blur', function() {
      console.log('blur event occurred!');

      if(elem.value.length < 2) {
        msg.innerHTML = '이름은 2자 이상이어야 합니다';
      } else {
        msg.innerHTML = '';
      }
    });
  </script>
</body>
</html>

2자 이상이라는 규칙이 바뀌면 이 규칙을 확인하는 모든 코드를 수정해야 한다. 따라서 이러한 방식의 코딩은 바람직하지 않다. 이유는 규모가 큰 프로그램의 경우 수정과 테스트에 소요되는 자원의 낭비도 문제이지만 수정에는 거의 대부분 실수가 동반되기 때문이다.

2자 이상이라는 규칙을 상수화하고 함수의 인수로 전달도록 수정하자. 이렇게 하면 규칙이 변경되어도 함수는 수정하지 않아도 된다.

그런데 addEventListener 메서드의 두번째 매개변수는 이벤트가 발생했을 때 호출될 함수이다. 이때 두번째 매개변수에는 함수 호출이 아니라 함수 자체를 지정하여야 한다.

function foo() {
  alert('clicked!');
}
// btn.addEventListener('click', foo()); // 이벤트 발생 시까지 대기하지 않고 바로 실행된다
elem.addEventListener('click', foo);     // 이벤트 발생 시까지 대기한다

따라서 전통적 DOM Event Handler 방식과 같이 함수에 인수를 전달할 수 없는 문제가 발생한다. 이를 우회하는 방법은 아래와 같다.

var MIN_USER_NAME_LENGTH = 2; // 이름 최소 길이

var elem = document.getElementById('username');
var msg  = document.getElementById('message');

function checkUserNameLength(n) {
  console.log('blur event occurred!');

  if(elem.value.length < n) {
    msg.innerHTML = '이름은 ' + n + '자 이상이어야 합니다';
  } else {
    msg.innerHTML = '';
  }
}

elem.addEventListener('blur', function() {
  checkUserNameLength(MIN_USER_NAME_LENGTH);
});

4. Event Flow (이벤트의 흐름)

계층적 구조에 포함되어 있는 HTML 요소에 이벤트가 발생할 경우 연쇄적 반응이 일어난다. 즉 이벤트가 전파(Event Propagation)되는데 전파 방향에 따라 버블링(Event Bubbling)과 캡처링(Event Capturing)으로 구분할 수 있다.

자식 요소에서 이벤트가 발생하여 부모 요소로 전파되는 것을 버블링이라 하고, 부모 요소에서 이벤트가 발생하여 자식 요소로 전파되는 것을 캡처링이라 한다. 캡처링은 IE8 이하에서 지원되지 않는다.

event flow

www.w3.org/TR/DOM-Level-3-Events

addEventListener() 메서드의 세번째 매개변수에 true를 설정하면 발생한 이벤트는 캡쳐링되고 false 또는 미설정하면 이벤트는 버블링된다.

<!DOCTYPE html>
<html>
<head>
  <style>
    html { border:1px solid red; padding:30px; text-align: center; }
    body { border:1px solid green; padding:30px; }
    .top {
      width: 300px; height: 300px;
      background-color: red;
      margin: auto;
    }
    .middle {
      width: 200px; height: 200px;
      background-color: blue;
      position: relative; top: 34px; left: 50px;
    }
    .bottom {
      width: 100px; height: 100px;
      background-color: yellow;
      position: relative; top: 34px; left: 50px;
      line-height: 100px;
    }
  </style>
</head>
<body>
  body
  <div class="top">top
    <div class="middle">middle
      <div class="bottom">bottom</div>
    </div>
  </div>
  <script>
  // true: capturing / false: bubbling
  var useCature = true;

  var handler = function(event) {
    var phases = ['capturing', 'target', 'bubbling']
    var node = this.nodeName + (this.className ? '.' + this.className : '');
    console.log(node, phases[event.eventPhase-1]);
    alert(node, phases[event.eventPhase-1]);
  }

  document.querySelector('html').addEventListener('click', handler, useCature);
  document.querySelector('body').addEventListener('click', handler, useCature);

  document.querySelector('div.top').addEventListener('click', handler, useCature);
  document.querySelector('div.middle').addEventListener('click', handler, useCature);
  document.querySelector('div.bottom').addEventListener('click', handler, useCature);
  </script>
</body>
</html>

5. Event 객체

event 객체는 이벤트를 발생시킨 요소와 발생한 이벤트에 대한 정보를 제공한다. 이벤트가 발생하면 event 객체는 동적으로 생성되며 이벤트를 처리할 수 있는 이벤트 리스너에 전달된다.

<!DOCTYPE html>
<html>
<body>
  <p>클릭하세요. 클릭한 곳의 좌표가 표시됩니다.</p>
  <em id='message'></em>
  <script>
  function showCoords(e) { // e: event object
    var msg  = document.getElementById('message');
    msg.innerHTML =
      "clientX value: " + e.clientX + "<br>" +
      "clientY value: " + e.clientY;
  }
  addEventListener('click', showCoords, false);
  </script>
</body>
</html>

위와 같이 이벤트를 처리할 함수 showCoords()에 event 객체를 매개변수로 전달하지 않았지만 event 객체는 암묵적으로 전달된다. 그러나 함수 선언에는 반드시 매개변수를 선언하여야 한다. 예제에서 e라는 이름으로 매개변수를 지정하였으나 다른 매개변수 이름을 사용하여도 상관없다.

이벤트 처리 함수에 매개변수를 전달할 필요가 있을 경우에는 익명함수 내 함수에 event 객체를 명시하여야 한다.

<!DOCTYPE html>
<html>
<body>
  <em id='message'></em>
  <script>
  function showCoords(e, msg) {
    msg.innerHTML =
      "clientX value: " + e.clientX + "<br>" +
      "clientY value: " + e.clientY;
  }

  var msg  = document.getElementById('message');

  addEventListener('click', function(e) {
    showCoords(e, msg)
  }, false);
  </script>
</body>
</html>

5.1 Event Property

5.1.1 Event.target

이벤트를 발생시킨 요소를 가리킨다.

<!DOCTYPE html>
<html>
<body>
  <button>Hide me</button>
  <script>
  function hide(e) {
    e.target.style.visibility = 'hidden';
  }

  document.querySelector('button').addEventListener('click', hide, false);
  </script>
</body>
</html>

5.1.2 Event.currentTarget

이벤트 리스너에 바인딩된 요소를 가리킨다.

이벤트 핸들러 함수 내의 this에는 이벤트 리스너에 연결된 요소가 바인딩된다. 따라서 이벤트 핸들러 함수 내에서 currentTarget과 this는 언제나 일치하다.

<!DOCTYPE html>
<html>
<head>
  <style>
    html, body { height: 100%; }
    div {
      height: 100%;
      width: 100%;
      padding: 10px;
    }
  </style>
</head>
<body>
  <div>
    <button>Change Background Color</button>
  </div>
  <script>
    function bluify(e) {
      console.log('this: ', this);
      console.log('e.target:', e.target);
      console.log('e.currentTarget: ', e.currentTarget);

      // Always true
      console.log(this === e.currentTarget);
      // true when currentTarget and target are the same object
      console.log(this === e.target);

      this.style.backgroundColor = '#A5D9F3';
    }
    // Get a list of every element in the document
    var elements = document.querySelector('div');
    elements.addEventListener('click', bluify, false);
  </script>
</body>
</html>

5.1.3 Event.type

발생한 이벤트의 종류를 나타내는 문자열을 반환한다.

<!DOCTYPE html>
<html>
<body>
  <em id='message'></em>
  <script>
  function getEventType(e) {
    console.log(e);
    document.getElementById('message').innerHTML = e.type + ' : ' + e.keyCode;
  }

  document.querySelector('body').addEventListener('keydown', getEventType, false);
  document.querySelector('body').addEventListener('keyup', getEventType, false);
  </script>
</body>
</html>

5.1.4 Event.cancelable

요소의 기본 동작을 취소시킬 수 있는지 여부(true/false)를 나타낸다.

5.2 Event Method

5.2.1 Event.preventDefault()

이벤트의 기본 동작을 취소한다. 단 Event.cancelable가 true일 경우에 한한다.

5.2.2 Event.stopPropagation()

이벤트의 전파(propagation: 버블링, 캡처링)를 중단한다.

6. Event Delegation (이벤트 위임)

우선 아래 예제를 살펴보자.

<ul id="parent-list">
  <li id="post-1">Item 1</li>
  <li id="post-2">Item 2</li>
  <li id="post-3">Item 3</li>
  <li id="post-4">Item 4</li>
  <li id="post-5">Item 5</li>
  <li id="post-6">Item 6</li>
</ul>

모든 li 요소가 클릭 이벤트에 반응하는 처리를 구현하고 싶은 경우, li 요소에 이벤트 리스너를 지정하면 총 6개의 이벤트 리스너를 등록하여야 한다. 만일 li 요소가 100개라면 100개의 이벤트 리스너를 등록하여야 한다.

이는 실행속도 저하의 원인이 될 뿐 아니라 코드 또한 매우 길어지며 작성 또한 불편하다.

이벤트 위임(Event Delegation)은 다수의 자식 요소에 이벤트 리스너를 등록하는 것 대신 하나의 부모 요소에 이벤트 리스너를 등록하는 방법이다. 위의 경우 6개의 자식 요소에 각각 이벤트 리스너를 등록하는 것 대신 부모 요소(ul#parent-list)에 이벤트 리스너를 등록하는 것이다.

또한 DOM 트리에 새로운 요소를 추가하더라도 이벤트 처리는 부모 요소에 위임되었기 때문에 새로운 요소에 이벤트를 핸들러를 다시 지정할 필요가 없다.

이는 이벤트가 이벤트 흐름에 의해 이벤트를 발생시킨 요소의 부모 요소에도 영향을 미치기 때문에 가능한 것이다.

발생시킨 요소를 알아내기 위해서는 Event.target을 사용한다.

<!DOCTYPE html>
<html>
<body>
  <ul id="parent-list">
    <li id="post-1">Item 1</li>
    <li id="post-2">Item 2</li>
    <li id="post-3">Item 3</li>
    <li id="post-4">Item 4</li>
    <li id="post-5">Item 5</li>
    <li id="post-6">Item 6</li>
  </ul>
  <div id="msg">
  <script>
    document.getElementById("parent-list").addEventListener("click", function(e) {
      console.log(e.target, e.target.nodeName);
      if(e.target && e.target.nodeName == "LI") {
        console.log("li#" + e.target.id + " was clicked!");
        document.getElementById("msg").innerHTML = "li#" + e.target.id + " was clicked!";
      }
    });
  </script>
</body>
</html>

7. 기본 동작의 변경

이벤트 객체는 요소의 기본 동작과 요소의 부모 요소들이 이벤트에 대응하는 방법을 변경하기 위한 메서드는 가지고 있다.

7.1 Event.preventDefault()

폼을 submit하거나 링크를 클릭하면 다른 페이지로 이동하게 된다. 이러한 요소가 가지고 있는 기본 동작을 중단시키기 위한 메서드가 preventDefault()이다.

<!DOCTYPE html>
<html>
<body>
  <a href="http://www.google.com">go</a>
  <script>
  document.querySelector('a').addEventListener('click', function(e) {
    console.log(e.target, e.target.nodeName);
    e.preventDefault();
  });
  </script>
</body>
</html>

7.2 Event.stopPropagation()

어느 한 요소를 이용하여 이벤트를 처리한 후 이벤트가 부모 요소로 버블링되는 것을 중단시키기 위한 메서드이다.

부모 요소에 동일한 이벤트에 대한 다른 핸들러가 지정되어 있을 경우 사용된다.

<!DOCTYPE html>
<html>
<body>
  <p>A paragraph with a <button>button</button>.</p>
  <script>
    var para = document.querySelector('p');
    var button = document.querySelector('button');
    para.addEventListener("mousedown", function() {
      console.log("Handler for paragraph.");
    });
    button.addEventListener("mousedown", function(event) {
      console.log("Handler for button.");

      if (event.which === 3){  // mouse right button clicked
        event.stopPropagation();
      }
    });
  </script>
</body>
</html>

부모 요소와 자식 요소에 모두 mousedown 이벤트에 대한 핸들러가 지정되어 있다. 하지만 부모 요소와 자식 요소의 이벤트를 각각 별도로 처리하고자 하는 경우 자식 요소의 이벤트가 부모 요소에게 전파되는 것을 방지할 필요가 있다.

7.3 preventDefault & stopPropagation

기본 동작의 중단과 버블링 또는 캡처링의 중단을 동시에 실시하는 방법은 아래와 같다.

return false;

단 이 방법은 jQuery를 사용할 때와 아래와 같이 사용할 때만 적용된다.

<!DOCTYPE html>
<html>
<body>
  <a href="http://www.google.com" onclick='return handleEvent()'>go</a>
  <script>
  function handleEvent() {
    return false;
  }
  </script>
</body>
</html>
<!DOCTYPE html>
<html>
<body>
  <div>
    <a href="http://www.google.com">go</a>
  </div>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.3/jquery.min.js"></script>
  <script>

  // within jQuery
  $('a').click(function (e) {
    e.preventDefault(); // OK
  });

  $('a').click(function () {
    return false; // OK --> return false does e.preventDefault() and e.stopPropagation().
  });

  // within pure js
  document.querySelector('a').addEventListener('click', function(e) {
    // e.preventDefault(); // OK
    return false;       // NG!!!!!
  });
  </script>
</body>
</html>

이 방법은 기본 동작의 중단과 이벤트 흐름의 중단 모두 적용되므로 이 두가지 중 하나만 중단하기 원하는 경우는 preventDefault() 또는 stopPropagation() 메서드를 개별적으로 사용한다.

Reference

Back to top
Close