49. 자바스크립트 이벤트 핸들링

소개

이제 당신은 CSS를 이용해서 레이아웃을 잡고 스타일을 주는데에 익숙할 것이다. 그리고 자바스크립트에서 변수와 함수, 메서드 등의 기본적인 개념을 이해하는 첫 발도 내딛었다 – 비록 그 첫 걸음이 좀 비틀거렸을 수도 있겠지만 말이다. 자 이제는, 그러한 지식을 이용해서 당신의 사이트를 방문하는 이용자들에게 상호작용적이면서 역동적인 행동을 제공할 때이다. 자바스크립트를 이용해서 이벤트를 조종하는 것은 당신에게 프랑켄슈타인 박사의 세계로 한걸음 내딛게 하며, 또한 당신의 창조물에게 진실로 생명을 부여하는 일이 될 것이다!

자바스크립트는 충분히 재미 있고, 또한 이 글을 통해 실용성을 갖게 될 것이다 – 이 글에서 당신은 이벤트가 무엇이며 그것을 당신의 페이지에서 어떻게 사용할 수 있는지 배우게 될 것이다. 목차는 다음과 같다:

  • 이벤트란 무엇인가?
  • 이벤트가 움직이는 방법
  • 이벤트의 혁명
    • DOM 레벨 2 이벤트들
    • IE의 이벤트 모델 예외
    • 다양한 브라우저에서 이벤트 등록하기
  • 이벤트와 접근성
  • 이벤트를 조종하기
    • 특정한 요소에 이벤트 적용하기
  • 이벤트 객체 레퍼런스
    • 이벤트에 종속적인 속성들을 체크해보기
  • 이벤트의 기본과 이벤트 버블링
    • 이벤트의 기본적 동작 차단
    • 이벤트 버블링의 중지
  • 완전한 이벤트 제어의 예제
  • 요약
  • 연습문제
이 글에서 사용된 코드 예제들을 다운로드해서 직접 연습해 볼 수 있다는 점을 잊지 말아줬으면 한다.

이벤트란 무엇인가?

웹 페이지에서는 어떤 종류의 상호작용이 발생할 때 이벤트가 일어난다. 사용자가 무언가를 클릭하거나 특정 요소 위로 마우스를 가져가거나 특정한 키를 누르는 것 등이 이러한 상호작용에 포함된다. 이벤트는 또한 브라우저에 의해서도 일어날 수 있는데, 웹 페이지의 로딩이 끝났다거나, 사용자가 페이지를 스크롤하거나, 브라우저 창의 크기를 조절하는 것 등이 포함된다.

자바스크립트의 사용 전체에 걸쳐서 당신은 어떤 특정 이벤트가 일어나는 것을 감지할 수 있으며, 그러한 이벤트들에 대응해서 어떤 일이 일어나게 될지 조합시킬수 있다.

이벤트가 움직이는 방법

웹 페이지 내의 HTML 요소에서 이벤트가 일어나게 되면, 그 요소는 이벤트에 대해 어떤 이벤트 핸들러가 할당되어 있는지 찾아보게 된다. 할당된 이벤트 핸들러가 있다면 적합한 방법으로 그것을 호출하게 되는데, 참조값과 일어난 이벤트에 대한 추가적인 정보들을 함께 전송한다. 그러면 이벤트 핸들러가 동작하게 된다.

이벤트 호출에는 두가지 타입이 있다: 캡춰링capturing과 버블링bubbling이 그것이다.

이벤트 캡춰링은 DOM 트리의 가장 바깥 요소에서부터 시작하여, 이벤트가 일어난 요소에 도착할 때까지 안쪽으로 찾아 들어가고, 다시 바깥으로 나온다. 예를 들어, 웹 페이지에서 무언가를 클릭하면, 처음에는 HTML 요소에서 onclick 이벤트 핸들러를 찾고, 다음에는 body 요소에서 찾고, 다음에는, 다음에는, 하는 식으로, 이벤트가 일어난 요소에 도착할 때까지 반복한다.

이벤트 버블링은 정확히 반대의 방법으로 동작한다: 이벤트가 일어난 요소에서부터 체크 – 이 요소에 뭔가 이벤트 핸들러가 할당되어 있는지 – 를 시작해서, 그 부모 요소로, 그 부모 요소로, 하는 식으로, HTML 요소까지 거슬러 올라간다.

이벤트의 혁명

자바스크립트의 초기에 우리들은 HTML 요소의 내부에서 직접적으로 이벤트 핸들러를 사용했다 – 이렇게 말이다:

<a href="http://www.opera.com/" onclick="alert('Hello')">Say hello</a>

이러한 접근 방법의 문제점은 이벤트 핸들러가 HTML 코드 전체에 섞여있는 것이다. 통제는 불가능에 가까우며, 외부 자바스크립트 파일을 인클루드해 올 때는 웹 브라우저의 캐쉬 기능을 전혀 사용할 수 없게 된다.

혁명적이라고 할 수 있었던 다음 단계는 자바스크립트 블럭에서 이벤트를 적용하게 된 것이다. 예를 들어:

<script type="text/javascript">
  document.getElementById("my-link").onclick = waveToAudience;
    function waveToAudience() {
      alert("Waving like I've never waved before!");
    }
</script>

<a id="my-link" href="http://www.opera.com/">My link</a>

마지막 예제에서의 깔끔한 HTML을 주의 깊게 보기 바란다. 이것이 일반적으로 겸손한 자바스크립트라 불리는 것이다. 이러한 깔끔한 HTML은 자바스크립트 캐싱과 코드 컨트롤은 차치하더라도 코드의 분리가 가져다 주는 장점은 크다: 내용이 한 곳에 모여있고, 상호작용을 처리하는 부분이 다른 곳에서 처리되고 있다. 이렇게 해 두면, 설령 자바스크립트가 꺼져 있다 하더라도 링크는 완벽하게 동작하게 되므로 더 높은 접근성을 획득하고, 또한 검색엔진을 기쁘게 할 수 있다.

DOM 레벨 2 이벤트

2000년 11월, W3C에서 Document Object Model (DOM) 레벨 2 이벤트 명세를 발표했다. DOM 레벨 2 이벤트는 웹 페이지에서 좀 더 세밀하고 자세하게 이벤트를 제어할 수 있는 방법을 제공한다. HTML 요소들에게 이벤트를 할당하는 새로운 방식은 이와 같은 모양이다:

document.getElementById("my-link").addEventListener("click", myFunction, false);

addEventListener method의 첫번째 매개변수는 이벤트의 이름인데, 접두사 “on” 을 더이상 사용하지 않고 있음을 주의 깊게 보기 바란다. 두번째 매개변수는 이벤트가 발생했을 때 호출하고자 하는 함수에 대한 참조이다. 세번째 매개변수는 이벤트에 대해 useCapture 라 칭하는 제어를 하는데, 쉽게 말해 이벤트 캡춰링을 사용할 것인지 아니면 버블링을 사용할 것인지 정해주는 것이다.

addEventListener의 반대는 removeEventListener이며, HTML 요소에 할당된 모든 이벤트를 제거한다.

IE의 이벤트 모델 예외

불행히도, IE는 DOM 레벨 2 이벤트 모델을 전혀 이행하지 않고 있으며, 그대신 그들만의 독자적인 attachEvent 메서드를 사용하고 있다. 코드는 다음과 같다:

document.getElementById("my-link").attachEvent("onclick", myFunction);
attachEvent 에서는 이벤트의 실제 이름 앞에 on 접두사를 여전히 붙이고 있음을 유의하기 바란다. 그리고 이것은 캡춰링/버블링을 결정할 근거에 대해 어떠한 지원도 하고 있지 않다.

attachEvent의 반대는 detachEvent이며, HTML 요소에 할당된 모든 이벤트를 제거한다.

다양한 브라우저에서 이벤트 등록하기

이벤트 핸들링을 실제 브라우저에 구현함에 있어서 브라우저들 간에 차이가 존재하므로, 모든 유명한 브라우저들에서 공통으로 이벤트 적용이 가능하도록 하는 해결책을 제시하고자 하는 수많은 시도들이 있었다. 이러한 해결책들은 각각 다른 장단점들이 있고, 보통은 addEvent 함수로 불린다.

유명한 자바스크립트 라이브러리들의 거의 대부분이 이러한 것을 포함하고 있으며, 온라인 상에는 이 기능만을 단독으로 갖고 있는 해결책들도 다수 존재한다. 하나 권한다면 Dean Edwards의 addEvent를 권하고 싶은데, jQuery 자바스크립트 라이브러리에서 이벤트를 다루는 옵션들 역시 꼭 읽어봐야 할 만한 것이다.

이벤트와 접근성

이벤트를 어떻게 호출하고 제어할지 더 깊이 파고들기 전에, 잠시 접근성 부분을 강조하고 싶다. 대부분의 사람들에게 접근성은 다소 넓은 주제이지만, 내가 여기에서 말하는 접근성이란, 자바스크립트가 꺼져 있거나 웹 서버에서 차단당하는 경우 등에도 이벤트는 반드시 동작해야 한다는 의미이다.

브라우저에서 자바스크립트를 꺼두는 사람들도 있긴 하지만, 대부분은 프락시 서버나 방화벽, 혹은 광신적인 안티바이러스 프로그램들이 자바스크립트를 프로그래머의 의도대로 동작하지 못하게 막고 있다. 이러한 사실들 때문에 좌절하지 말라: 내 목적은 자바스크립트가 불가능한 상황에서도 사용자가 여전히 사이트를 사용할 수 있게끔 에러처리fallback 옵션을 제공할 수 있도록 당신을 이끄는 것이다.

일반적으로, 특정 이벤트에 대해서 내장된 행동방식을 갖고 있지 않은 HTML 요소들에 이벤트를 할당하지 말아야 한다. 즉, onclick 이벤트는 a 와 같은 요소에만 사용해야 한다 – a 요소는 이미 클릭 이벤트에 대해 에러처리 방법(링크에 정의된 곳으로 이동한다거나, 입력양식을 제출한다거나)을 갖고 있기 때문이다.

이벤트 제어

이벤트의 간단한 예제와 당신이 그것에 대해 어떻게 반응할 수 있는가로부터 시작해보자. 간결함을 위해서 위에 제시했던 addEvent 함수를 사용해서 매 예제마다 크로스 브라우징의 복잡함 속으로 파고드는 일을 피하고자 한다.

우리의 첫번째 예제는 onload 이벤트이다. 이것은 window 객체에 종속된다. 일반적으로, 브라우저 윈도우에 영향을 미치는 모든 이벤트(예를 들어 onload, onresize, onscroll 등)는 window 객체를 통해 제어할 수 있다.

onload 이벤트는 웹 페이지에 포함된 모든것들이 완전히 로드되었을때 발생한다. 모든것이라 함은 HTML 코드 자신과 함께 이미지, CSS파일, 자바스크립트 파일과 같은 외부 참조들을 말한다. 그런것들이 전부 로드되었을때 window.onload 이벤트가 발생하며, 이 이벤트에 대응해서 웹 페이지의 특정 기능이 실행되도록 할 수 있다. 다음의 아주 간단한 예제는 페이지가 로드되었을때 메세지를 보여주는 것이다.

addEvent(window, "load", sayHi);
function sayHi() {
  alert("Hello there, stranger!");
}

그다지 나쁘지 않다. 당신이 원한다면, 소위 익명 함수를 사용해서 함수에 이름을 붙이지 않을수도 있다. 이렇게:

addEvent(window, "load", function () {
  alert("Hello there, stranger!");
});

특정한 요소에 이벤트 적용하기

이것을 좀 더 확장해보려면, 페이지에 있는 다른 요소들에 이벤트를 할당하는 법에 대해 알아 봐야만 한다. 이러한 논의를 위해 링크를 클릭 할 때마다 특정한 이벤트가 발생하길 원한다고 가정해 보자. 우리가 앞에서 배웠던 것과 이것을 결합해 보면 다음과 같이 하면 될 것이다:

addEvent(window, "load", function () {
  var links = document.getElementsByTagName("a");
    for (var i=0; i<links.length; i++) {
      addEvent(links[i], "click", function () {
        alert("NOPE! I won't take you there!");

        // This line`s support added through the addEvent function. See below.
      evt.preventDefault();
    });
  }
});

자 무슨 일이 일어났는가? 첫번째로는 onload 이벤트를 사용해서 페이지가 완전히 로딩된 시점을 알아냈다. 다음으로는 document 객체의 getElementsByTagName 메서드를 이용해서 페이지 내에 있는 모든 링크들을 찾아냈다. 이미 참조관계가 만들어져 있으므로, 모든 링크들에 대해 루프를 돌려서 이벤트를 할당했으므로 링크가 클릭될 때마다 액션이 일어난다.

그렇다면 “NOPE! I won`t take you there!”는 조크는 어떻게 된 건가? alert 아래의 줄을 이렇게 읽으면 된다 : return false. 이 문맥 속에서 이 말의 의미는, false를 돌려주는 것은 기본 액션이 실행되지 않도록 막는 것이다.

이벤트 객체 참조

당신의 이벤트 핸들링을 좀 더 자세하게 하기 위해서, 일어나는 이벤트의 특정한 부분별로 다른 액션을 할당해 줄 수 있다. 예를 들어, onkeypress 이벤트를 다루고 있다면, enter 에 대해서만 동작하고 다른 키에 대해서는 동작하지 않게끔 만들고 싶을 수도 있을 것이다.

이벤트 모델에 대해, 다른 모든 브라우저들이 W3C 권고안에 따라 이벤트 객체를 특정(일어난) 이벤트로만 전달하고 있는데 반해, 인터넷 익스플로러는 event 라는, 그들만의 독자적인 전역적 이벤트 객체를 사용하고 있다. 이러한 부분에서 발생하는 가장 흔한 문제는 이벤트 자신에 대한 참조와, 그 이벤트가 가리키고 있는 요소에 대한 참조를 얻는 일이다. 아래의 코드가 이런 부분에서 당신을 도와줄 것이다:

addEvent(document.getElementById("check-it-out"), "click", eventCheck);
function eventCheck (evt) {
  var eventReference = (typeof evt !== "undefined")? evt : event;
  var eventTarget = (typeof eventReference.target !== "undefined")? eventReference.target : eventReference.srcElement;
}

eventCheck 함수의 첫 줄은 함수로 이벤트 객체가 전달되었는지 체크한다. 전달되었다면, 이벤트는 자동적으로 함수의 첫번째 매개변수가 되고, 이 예제에서는 evt 라는 이름을 갖는다. 전달되지 않았다면, 현재 웹 브라우저가 인터넷 익스플로러라는 뜻이며, window 객체의 전역 속성인 event 를 참조한다.

이벤트 참조가 만들어졌으므로 두번째 줄에서 그것의 target 속성을 찾는다. 존재하지 않는다면, 인터넷 익스플로러가 생성하는 srcElement 를 사용한다.

이러한 제어와 행동은 위에서 언급한 addEvent 함수에서도 다루고 있는데, addEvent 함수에서도 이벤트 객체가 모든 브라우저에서 동작하도록 평준화되고 있다. 위의 코드는 마치 그렇지 않은 것처럼 작성했는데, 브라우저들 간의 차이에 대해 당신이 감각을 가질 수 있게끔 하기 위해서이다.

이벤트에 종속적인 속성 체크

자, 실행에 옮겨 보자. 다음의 코드는 사용자가 누르는 키에 따라 다른 반응을 보인다:

addEvent(document.getElementById("user-name"), "keyup", whatKey);
function whatKey (evt) {
  var eventReference = (typeof evt !== "undefined")? evt : event;
  var keyCode = eventReference.keyCode;
  if (keyCode === 13) {
    // The Enter key was pressed
    // Code to validate the form and then submit it
  }
  else if (keyCode === 9) {
    // The Tab key was pressed
    // Code to, perhaps, clear the field
  }
}

whatKey 함수의 코드는 일어난 이벤트의 속성 – keyCode – 을 체크해서, 실제로 눌린 키가 어떤것인지 알아본다. 13이란 숫자는 Enter 키를 의미하고, 9라는 숫자는 Tab 키를 의미한다.

이벤트 기본값과 버블링

이벤트의 기본적인 동작방식을 멈추고 싶은 경우가 아주 많이 있을 것이다. 예를 들어, 입력양식에 공란이 있는채로 제출하는 것을 막고 싶을수 있다. 이벤트 버블링의 경우에도 마찬가지인데, 이러한 상황을 어떻게 제어하는지 알아보도록 하자.

이벤트의 기본적 동작방식 차단

이벤트 모델과 이벤트 객체가 서로 다른 것과 마찬가지로, 이것을 IE에서 하는것과 다른 브라우저에서 하는 두가지 방법이 있다. 앞의 코드에서 이벤트 객체 참조를 찾아내는 코드를 작성했으므로, 이번에는 링크가 클릭되었을 때 링크 요소의 기본적인 동작방식을 막는 코드를 만들어보겠다.

addEvent(document.getElementById("stop-default"), "click", stopDefaultBehavior);
function stopDefaultBehavior (evt) {
  var eventReference = (typeof evt !== "undefined")? evt : event;
  if (eventReference.preventDefault) {
    eventReference.preventDefault();
  }
  else {
    eventReference.returnValue = false;
  }
}

위의 접근방법은 소위 디텍션, 이라 칭하는 것을 사용하는데, 메서드를 호출하기 전에 먼저 그것이 사용가능한지를 체크함으로서 에러를 막는 방법이다. preventDefault 메서드는 IE를 제외한 모든 브라우저에서 동작하며, 이것은 이벤트의 기본 동작이 일어나는 것을 막는다.

만약 이 메서드가 지원되지 않는다면, 전역 이벤트 객체의 returnValue 속성에 false 값을 할당하고, 따라서 이것이 IE에서 (이벤트의)기본 동작을 막아줄 것이다.

이벤트 버블링의 중지

다음 HTML의 계층구조를 음미해 보자:

<div>
  <ul>
    <li>
      <a href="http://www.opera.com/">Opera</a>
    </li>
    <li>
      <a href="http://www.opera.com/products/dragonfly/">Opera Dragonfly</a>
    </li>
  </ul>
</div>

a, li, ul 요소 모두에게 onclick 이벤트를 할당했다고 가정해보자. onclick 이벤트는 가장 먼저 링크의 이벤트 핸들러를 호출할 것이고, 다음으로 li, 다음으로 ul의 이벤트 핸들러를 호출할 것이다.

아마도, 사용자는 링크를 클릭할 것인데, 링크의 부모 요소인 li에 할당된 어떤 이벤트 핸들러도 동작하지 않게끔 하고 싶을 것이다. 반대로, 사용자가 링크가 아니라 그 옆에 있는 li 를 클릭한 경우에는 li 뿐 아니라 ul 요소에 할당한 이벤트 핸들러도 함께 동작하게끔 하고 싶을 수 있다.

DOM 레벨 2 이벤트 모델을 사용하고 useCapture 를 켜 뒀다면 – 다시 말해, 이벤트 캡춰링을 사용하고 있다면 – , ul → li → 링크 순서가 될 것임을 기억해 두기 바란다. 그렇긴 한데, IE는 이벤트 캡춰링을 지원하지 않으므로, 이런 일이 현실에서 일어나는 경우는 지극히 드물 것이다.

이벤트 버블링을 막기 위한 코드가 여기 준비되어 있다:

addEvent(document.getElementById("stop-default"), "click", cancelEventBubbling);
function cancelEventBubbling (evt) {
  var eventReference = (typeof evt !== "undefined")? evt : event;
  if (eventReference.stopPropagation) {
    eventReference.stopPropagation();
  }
  else {
    eventReference.cancelBubble = true;
  }
}

이벤트 핸들링의 완전한 예제

이벤트 핸들러를 추가하고, 특정 상황에서는 해당 이벤트의 기본동작을 막는, 예제 페이지 를 동봉하였다. 이벤트 핸들러는 사용자가 모든 필드를 작성하였는가 에 따라 폼이 제출될 수 있는지의 여부를 판단한다. 자바스크립트 코드는 다음과 같다:

addEvent(window, "load", function () {
  var contactForm = document.getElementById("contact-form");
  if (contactForm) {
    addEvent(contactForm, "submit", function (evt) {
      var firstName = document.getElementById("first-name");
      var lastName = document.getElementById("last-name");
      if (firstName && lastName) {
        if (firstName.value.length === 0 || lastName.value.length === 0) {
          alert("You have to fill in all fields, please.");
          evt.preventDefault();
        }
      }
    });
  }
});

요약

이 글에서는 이벤트 핸들링의 아주 기초적인 부분만을 다루었지만, 이벤트가 어떻게 동작하는지에 대해 잘 이해해주었기를 바란다. 브라우저 간의 비호환성을 다룬 것이 당신에게 조금 어렵게 느껴졌을수도 있겠지만, 내가 생각하기에 시작부터 이러한 문제들을 인식한다는 것은 대단히 중요하다.

일단 당신이 이러한 문제들을 인식하고, 위에서 제시한 해결책들을 마스터했다면, 당신은 자바스크립트와 이벤트 핸들링을 가지고 무엇이든 해낼 수 있을 것이다!

연습문제

  • 이벤트란 무엇인가?
  • 이벤트 캡춰링과 버블링의 차이는 무엇인가?
  • 이벤트의 실행을 제어할 수 있는가? 다시 말해, 그 기본적인 동작방식을 차단할 수 있는가? 어떻게 가능한가?
  • 자바스크립트 웹 커뮤니티를 들뜨게 한, attachEvent 와 scope 의 차이는 무엇인가?

저자에 대해

Robert Nyman은 웹 인터페이스 개발 분야에서 10년 동안 일해오고 있으며, 그가 가장 관심있어 한 주제는 자바스크립트이다. 그는 자신의 블로그, Robert’s talk 에 웹 개발에 대해 열성적으로 포스팅하고 있다.

그는 스웨덴에서 멋진 가족들과 함께 살고 있으며, 종종 가족들이 잠든 늦은 시간까지 집필에 힘쓴다. 또한 그가 꿈꾸고 있는 비밀스러운 소망은, 언젠가는 돈을 많이 벌어서 웹 세계의 경계를 넘어 현실 세계에 대한 글을 써 보는 것이다.

이 글을 다 읽어주셨다면, 댓글을 남겨주세요. 좋았다라는 격려도 좋고, 잘못된 부분을 지적해 주시는 것도 좋습니다. 마음에 드셨다면 아래 Like 버튼을 눌러서 페이스북과 트위터로 소개해 주시면 더욱 좋겠습니다.