이벤트 시스템

JavaScript의 이벤트 시스템과 위임(Delegation)에 대해서 알아봅니다. 코드간의 의존성을 줄이는 소프트웨어 개발 원리에 대해서 배웁니다. 또 크로스 브라우징 문제에 대해 알아봅니다.
이벤트 시스템, 위임, 의존성 분리, 크로스 브라우징


이벤트 시스템

웹 브라우저의 이벤트웹 브라우저의 이벤트

사건 기반 프로그래밍(Event-driven programming)은 프로그램의 흐름을 사건의 발생에 따라서 제어하는 프로그래밍 패턴을 말합니다. GUI를 제공하는 대부분의 플랫폼은 이벤트 관련 API를 제공하여 개발자가 사용자와 프로그램의 상호작용 제어 할 수 있도록 합니다. 또한 GUI 외에도 모듈간의 결합성을 분리(Decoupling)하기 위해서 이벤트 기반의 인터페이스를 도입하는 경우도 많습니다.

이벤트와 이벤트 핸들러이벤트와 이벤트 핸들러

웹 브라우저는 사용자의 입력에 따라서 입력 정보가 담긴 이벤트 객체를 생성하고, 이를 이벤트 핸들러라고 하는 함수에 전달합니다. 이벤트 핸들러는 이벤트 객체의 데이터에 따라서 사용자가 기대 할 만한 작업을 수행합니다.
웹 페이지가 로드되거나, 웹 브라우저의 창 크기를 조절하거나, DOM을 클릭하거나, 마우스를 특정 DOM에 올려 놓거나, 키보드를 입력하거나 하는 행위는 특정 타입의 이벤트를 발생(Trigger)시킵니다. 이때 해당 타입의 이벤트에 대한 핸들러가 바인딩(Binding)된 DOM 객체는 이벤트 객체를 전달 받고 이벤트 핸들러를 호출하게 됩니다. 여기서 이벤트 핸들러(Event Handler)라고 하는 함수는 일반적인 함수이며 다른 말로 이벤트 리스너(Event Listener)라고도 합니다.

발행-구독 패턴

이러한 이벤트 시스템 같은 디자인 패턴을 옵저버 패턴(Observer Pattern) 또는 발행-구독 패턴(Publish-Subscribe Pattern)이라고도 합니다. 웹 브라우저에서는 웹 브라우저 자체가 옵저버/발행자(Observer/Publisher)이고, 이벤트 핸들러를 등록한 개별 DOM 객체들이 구독자(Subscriber)가 됩니다. 프로그램에서 이벤트가 발생하고 처리되는 과정을 축약하면 다음과 같습니다.

  • 사용자나 코드가 특정 타입의 이벤트를 발생시킴
  • Observer/Publisher는 특정 타입의 이벤트를 구독하고 있는 Subscriber들의 이벤트 핸들러를 실행

DOM에 이벤트 핸들러를 바인딩하기

주요 DOM 이벤트 목록

분류타입설명
Mouseclick클릭
contextmenu우클릭
dblclick더블 클릭
mousedown누른 상태
mouseup누르고 뗐을 때
mouseoverElement나 그 자식들 위에 마우스를 위치 할 때
mousemove그 안에서 이동 할 때
mouseout그 안에서 나왔을 때
Keyboardkeydown키를 누른 상태 (계속 발생)
keypress문자에 해당되는 키를 누른 상태 (계속 발생)
keyup누르고 뗐을 때
FormfocusInput에 커서가 맞춰졌을 때
blurfocus가 풀렸을 때
changeInput value가 변경 될 때
resetform을 reset시켰을 때
submitform을 submit시켰을 때
FormfocusInput에 커서가 맞춰졌을 때
blurfocus가 풀렸을 때
changeInput value가 변경 될 때
resetform을 reset시켰을 때
submitform을 submit시켰을 때
Frame/Objectload페이지나 데이터가 로드되었을 때
beforeunload페이지가 unload(닫기)되기 직전에
unload페이지가 unload(닫기)될 때
Drag생략마우스 드래그 관련
Touch생략터치(스마트폰 등) 관련
Clipboard생략클립보드 관련
Print생략인쇄 관련

Element 뿐만 아니라 window 객체나 iframe, object 태그가 생성하는 Frame, Object 객체나 또한 이벤트 시스템의 일원입니다. DOM 이벤트에 대한 상세 목록은 W3C HTML DOM 이벤트 에서 확인 할 수 있습니다.

핸들러가 실행될 때 이벤트 객체가 핸들러의 첫째 인자로 넘어오게 됩니다. 위 문서를 참조하거나 이벤트 객체를 콘솔로 출력해보면 수 많은 속성이 있는 것을 확인할 수 있습니다. keyup 이벤트의 어떤 키가 눌렸는지, click 이벤트의 어느 좌표를 클릭했는지 같이 이벤트 타입별로 가질 수 있는 독특한 속성도 있고, 이벤트 타입을 나타내는 .type이나 이벤트 발생한 근원 DOM을 가리키는 .target같은 공통적인 속성도 있습니다. 이러한 정보를 기반으로 목적에 맞게 핸들러를 작성하면 됩니다.

의존성 분리와 이벤트 시스템

이벤트 시스템, 옵저버 패턴은 객체/모듈간의 결합을 분리(Decoupling)하는데 도입하기도 합니다. 이를 통해 프로그램을 구조적으로 유연하게 구현 할 수 있습니다.

객체간의 의존성 분리

예를 들어서 움직이는 공들이 있고, 공들은 서로 충돌 할 수 있다고 합시다.

var balls = [...];

var Ball = function(...){
    //...
};
Ball.prototype.checkCollision = function(){
    var collision = false;
    
    balls.forEach(ball => {
        // ...
    });

    return collision;
};

이런 구조는 모든 ball 인스턴스가 배열 balls에 의존성을 갖게 하며 반복적인 계산으로 비용을 키웁니다. 이런 상태를 결합도가 높다고(coupled)합니다. 한개의 공은 자신의 위치와 충돌이 발생했을 때 어떻게 반응 할지(이벤트 핸들러)만 알고 있으면 좀 더 자연스럽습니다.

var CollisionObserver = function(...){
    this.objects = [];
    // ...
}
CollisionObserver.prototype.addObject = function(...){ ...  };
CollisionObserver.prototype.checkCollision = function(...){
    // ...
    this.objects.forEach(function(obj){
        // ...
        if (....) {
            var eventData = {...};
            obj.onCollision(eventData); // 이벤트를 발생(Trigger)!
        }
    });
};

var Ball = function(...){...};
Ball.prototype.onCollision = function(e){...};

var Square = function(...){...};
Square.prototype.onCollision = function(e){...};

var Triangle = function(...){...};
Triangle.prototype.onCollision = function(e){...};

이후 공들을 관리하는 하나의 옵저버(Observer)가 주기적으로 공들의 충돌을 계산하고, 충돌 시 해당 공들에 이벤트를 발생시키면 되겠습니다. 이러한 구조로 확장성을 키우고, 계산 비용을 절감하며, 로직을 자연스럽게 할 수 있습니다.

모듈간의 의존성 분리

공지사항을 등록하고 관리하는 모듈이 있습니다. 이 모듈에서 공지사항이 등록될 때마다 고객들에게 이메일을 보내고 싶습니다.

// 모듈 자체가 AnyEmailModule에 의존하고 있음
Announcement.prototype.create = function(...){
    // ... 공지사항 등록 후

    AnyEmailModule.sendEmail(...);
};

위 처럼 구현 할 수 있겠습니다. 이때 Announcement 모듈은 AnyEmailModule에 의존성을 갖게 됩니다. 추후에AnotherEmailModule로 이메일 모듈을 교체하고, 이메일 뿐만 아니라 AnyPushModule로 스마트폰 알림을 주고 싶습니다.

// 모듈 자체가 AnotherEmailModule, AnyPushModule에 의존하고 있음
Announcement.prototype.create = function(...){
    // ... 공지사항 등록 후

    AnotherEmailModule.sendEmail(...);
    AnyPushModule.sendPush(...);
};

그렇게 나쁘지 않습니다만, 시스템이 복잡해지면 데이터의 흐름을 파악하기 힘들어 질 수 있습니다. 이럴 때 이벤트 시스템을 도입하면 더 결합도가 낮고 확장성 있는 코드를 유지 할 수 있습니다. 이러한 개념은 백엔드를 다룰 때 다시 다뤄보겠습니다.

// Announcement 모듈 자체는 의존하는 타 모듈이 없음
Announcement.prototype.create = function(...){
    // ... 공지사항 등록 후

    var eventData = { ... };
    this.trigger('afterCreate', eventData); // 이벤트 핸들러들을 실행
};

Announcement.prototype.addHandler = function(eventType, handler){
    this.handlers[eventType].push(handler);
};

Announcement.prototype.trigger = function(eventType, eventData){
    this.handlers[eventType].forEach(handler => {
        handler.call(this, eventData); // Function의 call, apply 메소드에 대해서는 다음에 알아봅니다.
    });
};

// ...

var announce1 = new Announcement(...);

// 모듈 자체가 아니라 인스턴스만 AnotherEmailModule, AnyPushModule에 의존하고 있음
// 이렇게 하면 Announcement는 좀 더 유연하게 사용 할 수 있는 모듈이 되겠습니다.
announce1.addHandler('afterCreate', function(e){
    AnotherEmailModule.sendEmail(...);
    AnyPushModule.sendPush(...);
});

커스텀 이벤트

JavaScript 코드로 위처럼 직접 이벤트 시스템을 구현해도 좋습니다. 하지만 웹 브라우저 DOM 이벤트 시스템을 그대로 이용하면서 이벤트 타입을 추가하고 싶다면 커스텀 이벤트 객체를 만들어서 이용 할 수 있습니다.

이벤트 전파

이벤트는 단순히 target에게 바로 전달 될 수도 있지만, GUI 이벤트 시스템에서는 구조적인 이벤트의 제어를 위해서 이벤트가 전파되는 흐름이 있습니다.

이벤트 전파 흐름이벤트 전파 흐름

  1. 캡처링 (Capturing)
    최상위 객체(window)에서부터 DOM 트리를 타고 내려오며 캡처링 속성의 이벤트 핸들러를 호출

  2. 타겟 (Target)
    이벤트가 dispatch된 요소의 이벤트 핸들러를 호출

  3. 버블링 (Bubbling)
    타겟에서 다시 반대로 최상위 객체(window)까지 DOM 트리를 타고 올라가며 버블링 속성의 이벤트 핸들러를 호출

이벤트 전파 예시

웹 브라우저의 이벤트 시스템을 이용 할 땐, 캡처링과 버블링 중, 버블링 단계의 이벤트 전파를 기본으로 생각하는 것이 좋습니다. 이벤트 핸들러를 등록할 때 디폴트가 버블링 속성이며 Internet Explorer 처럼 캡처링 단계의 전파를 지원하지 않는 브라우저가 있기 때문입니다.

event.stopPropagation()


이벤트 핸들러에서 이벤트 객체의 .stopPropagation() 메소드를 호출하면 이벤트의 전파를 멈출 수 있습니다.

event.preventDefault()

(커스텀 이벤트 외에) 이벤트 핸들러에서 이벤트 객체의 .preventDefault() 메소드를 호출하면 이벤트에 따른 브라우저의 기본 작동을 방지 할 수 있습니다.

위임

위임(Delegation)이라 불리는 디자인 패턴이 있습니다. 위임은 말 그대로 객체의 일부 작업을 다른 객체에게 맡긴다는 것입니다. GUI의 이벤트 처리에 위임을 적용하면 계산 비용을 절감 할 뿐만 아니라, 확장성 있는 구조를 만들 수 있습니다.
구체적으로, 부모 요소에 여러 자식 요소들이 있다고 할 때, 모든 자식 요소들이 스스로 이벤트 처리를 하는 것은 큰 비용이 되거나, 프로그램의 구조를 해칠 수 있습니다. 이 때 자식 요소들의 이벤트 처리를 부모에게 위임 할 수 있습니다. 이는 자식 요소들에서 발생한 이벤트가 부모 요소에까지 전파되기 때문에 가능한 패턴입니다.

위임을 적용한 이벤트 처리

크로스 브라우징

크로스 브라우징(Cross Browsing)은 웹 브라우저간 호환성에 문제가 없음을 의미하는 단어입니다. 웹 브라우저간 지원하는 JavaScript의 버전의 차이, 웹 표준을 따르지 않는 API(중요한 예로 IE의 JavaScript API에는 DOM 요소에 .addEventListner 라는 메소드가 없고 .attachEvent 라는 메소드가 있습니다.) 등으로, 다양한 웹 브라우저를 사용하는 유저들을 모두 만족시키려면 개발자의 애로사항이 큽니다.

이를 해결하기 위해서는 Shim/Polyfill으로 불리는 native API의 간극을 메꾸어주는 라이브러리를 쓸 수도 있고, jQuery 같이 native API의 크로스 브라우징 문제를 해결하면서, DOM 조작 등 스크립팅의 편의를 위한 라이브러리를 이용 할 수도 있습니다.

목차
3. 웹 프론트엔드
4. 웹 백엔드
저자

김동욱

개발 경력 약 10년, 전문 분야는 웹 및 각종 응용 소프트웨어 플랫폼입니다. Codeflow를 운영하고 있습니다.

2018년 04월 10일 업데이트

지원되지 않는 웹 브라우저거나 예기치 않은 오류가 발생했습니다.