본문 바로가기
JS/JS 공부는 다다익선 [모던자바스크립트]

JS 공부는 다다익선 - 12. 이벤트

by spare8433 2023. 10. 4.

1. 이벤트의 이해

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

 

브라우저는 이벤트를 감지할 수 있으며 이벤트 발생 시에는 통지해 준다. 이 과정을 통해 사용자와 웹페이지는 상호작용(Interaction)이 가능하게 된다. 이벤트는 일반적으로 함수에 연결되며 그 함수는 이벤트가 발생하기 전에는 실행되지 않다가 이벤트가 발생되면 실행된다. 이 함수를 이벤트 핸들러라 하며 이벤트에 대응하는 처리를 기술한다.



2. 이벤트 루프(Event Loop)와 동시성(Concurrency)

이벤트 루프(Event Loop)는 이벤트 리스너를 관리하고, 이벤트가 발생하면 해당 이벤트 핸들러를 호출합니다. 그리고 비동기 코드 관리를 담당하며, 비동기 작업을 웹 API로 보내거나 큐에 대기시키고 적절한 시점에 호출 스택으로 옮겨 실행하는 역할(이벤트 교통정리 역활)을 합니다.



브라우저는 단일 쓰레드(single-thread)에서 이벤트 드리븐(event-driven) 방식으로 동작한다.

 

event-driven : 분산된 시스템에서 이벤트를 생성(발행)하고 발행된 이벤트를 수신자에게 전송하는 구조로 수신자는 그 이벤트를 처리하는 방식의 아키텍처입니다.



단일 쓰레드는 쓰레드가 하나뿐이라는 의미이며 이 말은 곧 하나의 작업(task)만을 처리할 수 있다는 것을 의미한다. 하지만 실제로 동작하는 웹 애플리케이션은 많은 task가 동시에 처리되는 것처럼 느껴진다. 이처럼 자바스크립트의 동시성(Concurrency)을 지원하는 것이 바로 이벤트 루프이다.




event-loop



2.1 자바스크립트 엔진

구글의 V8을 비롯한 대부분의 자바스크립트 엔진은 크게 Call Stack(호출 스택)Heap 영역으로 나뉜다.

 

자바스크립트 엔진은 단순히 작업이 요청되면 Call Stack 을 사용하여 요청된 작업을 순차적으로 실행한다.

 

동시성(Concurrency)을 지원하기 위해 필요한 비동기 요청(이벤트를 포함) 처리자바스크립트 엔진을 구동하는 환경 즉 브라우저(또는 Node.js)가 담당한다.



- Call Stack(호출 스택)

작업이 요청되면(함수가 호출되면) 요청된 작업은 순차적으로 Call Stack에 쌓이게 되고 순차적으로 실행된다. 자바스크립트는 단 하나의 Call Stack을 사용하기 때문에 해당 task가 종료하기 전까지는 다른 어떤 task도 수행될 수 없다.



- Heap

동적으로 생성된 객체 인스턴스가 할당되는 영역이다.



- Event Queue(Task Queue)

비동기 처리 함수의 콜백 함수, 비동기식 이벤트 핸들러, Timer 함수(setTimeout(), setInterval())의 콜백 함수가 보관되는 영역으로 이벤트 루프(Event Loop)에 의해 특정 시점(Call Stack이 비어졌을 때)에 순차적으로 Call Stack으로 이동되어 실행된다.



- Microtask Queue

promise.then, process.nextTick, MutationObserver 와 같이 우선적으로 비동기로 처리되는 함수들의 콜백 함수가 들어가는 큐 (처리 우선순위가 높음)



※ 핵심은 호출 스택이 완전히 비워진 후에 이벤트 큐에 대기중인 내용을 호출 스택에 가져와서 실행한다. 실제로 대기과정에서 딜레이가 생길 수 있다.



2.2 Event Loop(이벤트 루프)

Call Stack 내에서 현재 실행중인 task가 있는지 그리고 Event Queuetask가 있는지 반복하여 확인한다. 만약 Call Stack이 비어있다면 Event Queue 내의 taskCall Stack으로 이동하고 실행된다.



예시

function func1() {
  console.log('func1');
  func2();
}

function func2() {
  setTimeout(function () {
    console.log('func2');
  }, 0);

  func3();
}

function func3() {
  console.log('func3');
}

func1();

// 결과 
// func1
// func3
// func2

 

함수 func1이 호출되면 함수 func1은 Call Stack에 쌓인다. 그리고 함수 func1은 함수 func2을 호출하므로 함수 func2가 Call Stack에 쌓이고 setTimeout가 호출된다.

 

setTimeout의 콜백함수는 즉시 실행되지 않고 지정 대기 시간만큼 기다리다가 “tick” 이벤트가 발생하면 태스크 큐로 이동한 후 Call Stack이 비어졌을 때 Call Stack으로 이동되어 실행된다.

 

※ DOM 이벤트 핸들러도 이와 같이 동작한다.



3. 이벤트의 종류

 

3.1 UI Event

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



3.2 Keyboard Event

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



3.3 Mouse Event

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



3.4 Focus Event

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



3.5 Form Event

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



3.6 Clipboard Event

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



4. 이벤트 핸들러 등록

 

4.1 인라인 이벤트 핸들러 방식

HTML 요소의 이벤트 핸들러 어트리뷰트에 이벤트 핸들러를 등록하는 방법이다.



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



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



CBD(Component Based Development) 방식의 Angular/React/Vue.js와 같은 프레임워크/라이브러리에서는 인라인 이벤트 핸들러 방식으로 이벤트를 처리한다. CBD에서는 HTML, CSS, 자바스크립트를 뷰를 구성하기 위한 구성 요소로 보기 때문에 관심사가 다르다고 생각하지 않는다.




여러 함수 호출 가능

 

  <button onclick="myHandler1(); myHandler2();">Click me</button>
  <script>
    function myHandler1() {
      alert('myHandler1');
    }
    function myHandler2() {
      alert('myHandler2');
    }
  </script>

 

4.2 이벤트 핸들러 프로퍼티 방식

이벤트 핸들러 프로퍼티에 함수를 등록하는 방식이다.

 

하나의 이벤트 핸들러만을 바인딩할 수 있다.



  <button class="btn">Click me</button>
  <script>
    const btn = document.querySelector('.btn');

    // 이벤트 핸들러 프로퍼티 방식은 이벤트에 하나의 이벤트 핸들러만을 바인딩할 수 있다
    // 첫번째 바인딩된 이벤트 핸들러 => 실행되지 않는다.
    btn.onclick = function () {
      alert('① Button clicked 1');
    };

    // 두번째 바인딩된 이벤트 핸들러 => 실행됨
    btn.onclick = function () {
      alert('① Button clicked 2');
    };

    // addEventListener 메소드 방식
    // 첫번째 바인딩된 이벤트 핸들러 => 실행됨
    btn.addEventListener('click', function () {
      alert('② Button clicked 1');
    });

    // 두번째 바인딩된 이벤트 핸들러 => 실행됨
    btn.addEventListener('click', function () {
      alert('② Button clicked 2');
    });
  </script>



4.3 addEventListener 메소드 방식

addEventListener 메소드를 이용하여 대상 DOM 요소에 이벤트를 바인딩하고 해당 이벤트가 발생했을 때 실행될 콜백 함수(이벤트 핸들러)를 지정한다.

 

장점

 

  • 하나의 이벤트에 대해 하나 이상의 이벤트 핸들러를 추가할 수 있다.
  • 캡처링과 버블링을 지원한다.
  • HTML 요소뿐만아니라 모든 DOM 요소(HTML, XML, SVG)에 대해 동작한다. 브라우저는 웹 문서(HTML, XML, SVG)를 로드한 후, 파싱하여 DOM을 생성한다.

addEventListener 메소드는 IE 9 이상에서 동작한다. IE 8 이하에서는 attachEvent 메소드를 사용한다.

 

addEventListener 의 매개변수로 전달할 콜백함수를 따로 분리해서 전달하는 경우 함수 호출이 아니라 함수 자체를 지정하므로 인수를 전달할 수 없으므로 함수로 감싸서 내부에서 호출하는 방식을 사용한다.



input.addEventListener('blur', function () {
  // 이벤트 핸들러 내부에서 함수를 호출하면서 인수를 전달한다.
  checkUserNameLength(MIN_USER_NAME_LENGTH);
});



5. 이벤트 핸들러 함수 내부의 this

 

5.1 인라인 이벤트 핸들러 방식

인라인 이벤트 핸들러 방식의 경우, 이벤트 핸들러는 일반 함수로서 호출되므로 이벤트 핸들러 내부의 this는 전역 객체 window를 가리킨다.



5.2 이벤트 핸들러 프로퍼티 방식

이벤트 핸들러 프로퍼티 방식에서 이벤트 핸들러는 메소드이므로 이벤트 핸들러 내부의 this는 이벤트에 바인딩된 요소를 가리킨다. 이것은 이벤트 객체의 currentTarget 프로퍼티와 같다.

 

<button class="btn">Button</button>
<script>
  const btn = document.querySelector('.btn');

  btn.onclick = function (e) {
    console.log(this); // <button id="btn">Button</button>
    console.log(e.currentTarget); // <button id="btn">Button</button>
    console.log(this === e.currentTarget); // true
  };
</script>



5.3 addEventListener 메소드 방식

addEventListener 메소드에서 지정한 이벤트 핸들러는 콜백 함수이지만 이벤트 핸들러 내부의 this는 이벤트 리스너에 바인딩된 요소(currentTarget)를 가리킨다. 이것은 이벤트 객체의 currentTarget 프로퍼티와 같다.

 

  <button class="btn">Button</button>
  <script>
    const btn = document.querySelector('.btn');

    btn.addEventListener('click', function (e) {
      console.log(this); // <button id="btn">Button</button>
      console.log(e.currentTarget); // <button id="btn">Button</button>
      console.log(this === e.currentTarget); // true
    });
  </script>



6. 이벤트의 흐름

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

 

자식 요소에서 발생한 이벤트가 부모 요소로 전파되는 것을 버블링이라 하고, 자식 요소에서 발생한 이벤트가 부모 요소부터 시작하여 이벤트를 발생시킨 자식 요소까지 도달하는 것을 캡처링이라 한다. 주의할 것은 버블링과 캡처링은 둘 중에 하나만 발생하는 것이 아니라 캡처링부터 시작하여 버블링으로 종료한다는 것이다. 즉, 이벤트가 발생했을 때 캡처링과 버블링은 순차적으로 발생한다.

 

이벤트는 기본적으로 버블링(Bubbling) 단계 전파됩니다.

 

※ 캡처링은 IE8 이하에서 지원되지 않는다.



event flow



addEventListener 메소드의 세번째 매개변수에 true 로 설정하면 캡처링으로 전파되는 이벤트를 캐치하고 false 또는 미설정하면 버블링으로 전파되는 이벤트를 캐치한다.

 

<p>버블링과 캡처링 이벤트 <button>버튼</button></p>
<script>
  const body = document.querySelector('body');
  const para = document.querySelector('p');
  const button = document.querySelector('button');

  // 버블링
  body.addEventListener('click', function () {
    console.log('Handler for body.');
  });

  // 캡처링
  para.addEventListener('click', function () {
    console.log('Handler for paragraph.');
  }, true);

  // 버블링
  button.addEventListener('click', function () {
    console.log('Handler for button.');
  });
</script>



위와 같이 캡처링과 버블링이 혼용해서 사용 할 수 있다.



7. Event 객체

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

 

event 객체는 이벤트 핸들러에 암묵적으로 전달된다.



7.1 Event Property

 

프로퍼티 내용
Event.target 실제로 이벤트를 발생시킨 요소를 가리킨다.
Event.currentTarget 이벤트에 바인딩된 DOM 요소를 가리킨다.
Event.type 발생한 이벤트의 종류를 나타내는 문자열을 반환
Event.cancelable 요소의 기본 동작을 취소시킬 수 있는지 여부(true/false)를 나타낸다.
Event.eventPhase 이벤트 흐름(event flow) 상에서 어느 단계(event phase)에 있는지를 반환한다. (0: 이벤트 없음, 1: 캡쳐링 단계, 2: 타깃, 3: 버블링 단계)



8. Event Delegation (이벤트 위임)

 

이벤트 핸들러를 특정 개별 요소에 연결하는 대신, 이벤트를 해당 요소의 상위 요소에 연결하여 처리하는 방식을 말합니다. 주로 동적으로 생성되는 요소나 여러 개의 요소에 대한 일괄적인 이벤트 처리에 유용합니다.

 

<ul id="myList">
    <li>항목 1</li>
    <li>항목 2</li>
    <li>항목 3</li>
    <li>항목 4</li>
</ul>

<script>   
    const myList = document.querySelector("#myList")

    // 리스트 컨테이너 요소에 클릭 이벤트 핸들러를 연결
    myList.addEventListener("click", function(event) {
        // 클릭된 요소가 <li> 태그인 경우에만 처리
        if (event.target.nodeName === "LI") {
            // 클릭된 항목의 내용을 콘솔에 출력
            console.log("클릭된 항목: " + event.target.textContent);
        }
    });
</script>



9. 기본 동작의 변경

 

9.1 Event.preventDefault()

요소가 가지고 있는 기본 동작을 중단시키기 위한 메소드

9.2 Event.stopPropagation()

이벤트가 부모 요소로 이벤트가 전파되는 것을 중단시키기 위한 메소드



참고

https://poiemaweb.com/js-event

https://inpa.tistory.com/entry/%F0%9F%94%84-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%A3%A8%ED%94%84-%EA%B5%AC%EC%A1%B0-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC

https://akasai.space/architecture/about_event_driven_architecture/