이벤트 핸들러(Event Handler) 정리

dyeon-dev·2025년 8월 28일

이벤트와 핸들러의 기본 개념

"이벤트"는 우리가 웹 페이지에서 마우스를 클릭하거나 키보드를 누르는 것과 같은 사용자 행동, 또는 인풋 창에 포커스가 맞춰지거나 문서 로드가 완료되는 것과 같이 특정 시점에 발생하는 모든 행동을 의미한다.
이러한 이벤트가 발생했을 때 특정 함수를 실행하도록 할당하는 것을 "핸들러"라고 한다.
이벤트가 발생했을 때 브라우저에게 이벤트 핸들러의 호출을 위임하는 것을 이벤트 핸들러 등록이라 한다.

이처럼 이벤트와 그에 대응하는 함수(이벤트 핸들러)를 통해 사용자와 애플리케이션은 상호작용을 할 수 있다. 이와 같이 이벤트 중심으로 제어하는 방식을 이벤트 드리븐 프로그래밍(event-driven programming)이라 한다.

🚀 이벤트 핸들러 할당 방식

사용자가 버튼을 "클릭"했을 때 메시지를 처리하고 싶다면,
개발자가 명시적으로 함수를 호출하는 것이 아니라 브라우저에게 함수 호출을 위임하는 것이다.
아래의 코드를 살펴보면 알겠지만 onclick 프로퍼티에 함수를 할당했다.

HTML 속성 방식 (어트리뷰트 방식)

html과 js를 분리하지 않는 방식이다.
onclick 어트리뷰트로 하게되면 함수 호출문을 직접 할당하는 방식이라 잘 쓰진 않는다.

하지만 CBD 방식의 Angular/React/Svelte/Vue.js 같은 프레임워크에서는 이벤트 핸들러 어트리뷰트 방식으로 이벤트를 처리한다. CBD에서는 HTML, CSS, JS가 다른 개별 요소가 아닌, 뷰를 구성하기 위한 요소로 보기 때문에 관심사가 다르다고 생각하지 않는다.

1. 인라인 코드로 작성

<button onclick="alert('click')">클릭</button>

2. onclick 속성에 직접 함수명 지정

  • 이벤트 핸들러 안에서 직접 실행할 코드를 문자열로 작성하는 방식이기 때문에 ()를 붙여야 실행 된다.
<button onclick="sayHello()">클릭</button>
<script>
  function sayHello() {
  	alert("Hello");
  }
</script>
  • onclick="sayHello()" 어트리뷰트는 파싱되어 다음과 같은 함수를 암묵적으로 생성하고, 이벤트 핸들러 어트리뷰트 이름과 동일한 키 onlick 이벤트 핸들러 프로퍼티에 할당한다.
function onclick(event) {
  sayHello();
}
  • 이처럼 동작하는 이유는 이벤트 핸들러에 인수를 전달하기 위해서다. 만약 이벤트 핸들러 어트리뷰트 값으로 함수 참조를 할당해야한다면 이벤트 핸들러에 인수를 전달하기는 곤란하다.
<!-- 이벤트 핸들러에 인수를 전달하기는 x -->
<button onclick="sayHello">클릭</button>

JS에서 할당 방식 (이벤트 핸들러 프로퍼티 방식)

html과 js를 분리하는 방식이다.
DOM 노드 객체는 이벤트에 대응하는 이벤트 핸들러 프로퍼티를 가지고 있다.
이벤트 핸들러 프로퍼티 키는 어트리뷰트와 마찬가지로 on+이벤트 종류를 나타내는 이벤트 타입으로 이뤄져있다.
이벤트 핸들러 프로퍼티에 함수를 바인딩하면 이벤트 핸들러가 등록된다.

3. DOM 요소에 ID를 부여한 후 자바스크립트에서 객체.핸들러 = 핸들러(함수) 할당

  • 이때 핸들러 할당 시 함수명 뒤에 괄호를 붙이지 않아야 함수 객체 자체를 직접 할당할 수 있다.
  • ()가 있으면, sayHello()가 즉시 실행된 결과값(여기서는 undefined)이 onclick에 들어가 버려서 크롬창 열자마자 alert 창이 떠버린다.
<button id="btn">클릭</button>
<script>
  const el = document.getElementById('btn')
  el.onclick = sayHello;
</script>

4. addEventListener

  • 하나의 이벤트에 여러 핸들러를 등록할 수 있기 때문에 가장 권장되는 방식이다.
  • addEventListener도 이벤트 핸들러를 등록하는 방식과 삭제하는 방식이 다양하다.

4-1. addEventListener(이벤트, 함수핸들러)

<button id="btn2">클릭</button>
<script>
  function sayHello() {
    alert("Hello");
  }
  
  const el = document.getElementById('btn2')
  el.addEventListener("click", sayHello);
</script>

4-2. addEventListener(이벤트, 함수 직접 작성) - 익명함수로 등록

<button id="btn2">클릭</button>
<script>
  const el = document.getElementById('btn2')
  el.addEventListener("click", () => {
  	alert("Hello");
  });
</script>

4-3. addEventListener 삭제

  • removeEventListener를 사용하여 할당된 핸들러를 삭제

💡 이벤트 리스너 삭제할 때 주요포인트

  • 익명 함수(() => { ... })로 등록한건 제거가 불가능하다.
<button id="btn2">클릭</button>
<script>
  const el = document.getElementById('btn2');
  // 익명 함수로 등록
  el.addEventListener("click", () => {
    alert("Hello");
  });
  // 아래 코드는 제거 불가능 (참조가 다르기 때문)
  el.removeEventListener("click", () => {
    alert("Hello");
  });
</script>
  • removeEventListener는 반드시 addEventListener에서 등록한 동일한 함수 객체 참조를 써야 작동하기 때문이다. JS 엔진은 겉보기엔 코드가 같아도, "완전히 별개 함수"로 인식하기 때문에 나중에 제거할 가능성이 있으면 별도 함수 선언/변수에 담아서 등록해야 한다.
<button id="btn2">클릭</button>
<script>
  function sayHello() {
    alert("Hello");
  }

  const el = document.getElementById('btn2');

  // 이벤트 등록
  el.addEventListener("click", sayHello);

  // 3초 후에 이벤트 제거
  setTimeout(() => {
    // 함수 참조로 등록 → 제거 가능
    el.removeEventListener("click", sayHello);
    alert("이제 클릭해도 반응 안 함!");
  }, 3000);
</script>

4-4. DOMContentLoaded처럼 문서 로드 완료 시 발생하는 이벤트의 경우, 반드시 addEventListener를 사용해야 한다.

  • HTML에 직접 할당하면 동작하지 않는다.
  • DOMContentLoaded 전용 핸들러 속성(onDOMContentLoaded)은 없다. document.onDOMContentLoaded = ... 이런 건 동작 X
  • 반면, load, beforeunload 같은 이벤트는 window.onload, window.onbeforeunload 같은 핸들러 속성도 제공된다.

💡 팁 : 가급적 addEventListener로 통일하는 것이 좋다.

아래에서 DOMContentLoaded와 관련된 문서 로딩 시점의 이벤트제어를 정리했다.


📃 문서 로딩 시점의 이벤트 제어(DOMContentLoaded, load, beforeunload, unload)

공식 문서에 따르면 HTML 문서의 생명주기엔 다음과 같은 3가지 주요 이벤트가 관여한다고 한다.

  • DOMContentLoaded : 브라우저가 HTML을 전부 읽고 DOM 트리를 완성하는 즉시 발생한다. 이미지 파일(<img>)이나 스타일시트 등의 기타 자원은 기다리지 않는다.
  • onload : HTML로 DOM 트리를 만드는 게 완성되었을 뿐만 아니라 모든 콘텐츠(images, script, css, etc)가 모두 불러오는 것이 끝났을 때 발생한다.
  • beforeunload/unload : 사용자가 페이지를 떠날 때 발생한다.

따라서 위의 순서대로 생명주기가 실행된다. 각각의 시점 이벤트이기 때문에 이벤트를 주입해서 사용해주면 된다.

// only document
window.addEventListener("DOMContentLoaded", (event) => {
    console.log("DOMContentLoaded");
});

// after resources (css, images)
window.addEventListener("onload", (event) => {
    console.log("load");
});

// before unload
window.addEventListener("beforeunload", (event) => {
    console.log("beforeunload");
});

// resource is being unloaded
window.addEventListener("unload", (event) => {
    console.log("unload");
});

주로 어떤 상황에서 활용될까?

DOMContentLoaded

DOM이 준비된 것을 확인한 후 원하는 DOM 노드를 찾아 핸들러를 등록해 인터페이스를 초기화할 때 사용된다.
앞에서 언급했던 것처럼 이 이벤트를 다루려면 addEventListener를 사용해야 한다.

<script>
  function ready() {
    alert('DOM이 준비되었습니다!');

    // 이미지가 로드되지 않은 상태이기 때문에 사이즈는 0x0입니다.
    alert(`이미지 사이즈: ${img.offsetWidth}x${img.offsetHeight}`);
  }

  document.addEventListener("DOMContentLoaded", ready);
</script>

<img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0">

위 예시에서 DOMContentLoaded은 DOM이 로드되었을 때 실행된다.
따라서 밑에 위치한 img뿐만 아니라 모든 요소에 접근할 수 있다. 하지만 이미지 파일의 로딩은 기다리지 않기 때문에 alert 창에는 이미지 사이즈가 0으로 뜬다.

이와 같은 특징을 브라우저 렌더링 과정에 대입해서 살펴보자.

💡 "렌더링 과정 중 DOM 트리가 완성되면 DOMContentLoaded 이벤트가 발생한다"

여기서 말하는 DOM 트리가 완성되는 단계를 브라우저가 동기적으로, 즉 위에서 아래로 순차적으로 실행되는 관점에서 좀 더 자세히 살펴보자.
이 과정에서 HTML을 파싱하면서 자바스크립트와 CSS를 만났을 때 DOMContentLoaded 이벤트처리는 어떻게 되는가?

1. HTML 문서를 처리하는 도중에 <script> 태그를 만났을 때

  • DOM 트리 구성을 중단하고 <script>를 실행한다. <script>가 끝나면 다시 HTML 문서 처리를 재개한다.
  • 따라서 DOMContentLoaded 이벤트 역시 <script> 안에 있는 스크립트가 처리되고 난 후에 발생한다.
<script>
  document.addEventListener("DOMContentLoaded", () => {
    alert("DOM이 준비되었습니다!");
  });
</script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"></script>

<script>
  alert("라이브러리 로딩이 끝나고 인라인 스크립트가 실행되었습니다.");
</script>
  • 위 코드의 결과를 보면, 스크립트가 모두 실행되고 나서야 DOMContentLoaded 이벤트가 발생한다.
  • 따라서 "라이브러리 로딩이 끝나고…"가 먼저 보인 후 "DOM이 준비되었습니다!"가 출력된다.

2. HTML 문서를 처리하는 도중에 외부 스타일시트를 만났을 때

  • HTML 파싱 중에 <link rel="stylesheet">를 만나면 CSS 파싱은 비동기적으로 진행될 수 있다. 즉, 외부 스타일시트는 DOM에 영향을 주지 않기 때문에 DOMContentLoaded는 외부 스타일시트가 로드되기를 기다리지 않는다.

  • 따라서 DOMContentLoaded 이벤트가 발생할 때 CSSOM이 모두 준비됐다는 보장은 없다.

  • 보통은 CSS 파일이 아직 다운로드 중이어도 DOMContentLoaded가 발생할 수 있다.

  • 💥 그런데 한가지 예외가 있다. 스타일시트를 불러오는 태그 바로 다음에 스크립트가 위치하면 이 스크립트는 스타일시트가 로드되기 전까지 실행되지 않는다.

<link type="text/css" rel="stylesheet" href="style.css">
<script>
  // 이 스크립트는 위 스타일시트가 로드될 때까지 실행되지 않습니다.
  // 스크립트에서 요소의 좌표 정보를 사용하고 있다.
  alert(getComputedStyle(document.body).marginTop);
</script>
  • <link><script> 사이에 다른 태그가 끼면, 스크립트 실행이 그 CSSOM을 보장해서 기다리지는 않는다.
  • 왜냐하면 그 시점에서는 해당 스크립트가 “바로 직전에 선언된 스타일시트와 직접 연관 있을 가능성”이 낮다고 보기 때문이다.

load

DOMContentLoaded 이벤트는 DOM만 보장했다면,
load 이벤트DOM + CSSOM + 이미지/폰트 같은 모든 리소스까지 준비 완료되었을 때 실행된다.

<script>
  window.onload = function() { // window.addEventListener('load', (event) => {와 동일합니다.
    alert('페이지 전체가 로드되었습니다.');

    // 이번엔 이미지가 제대로 불러와 진 후에 실행됩니다.
    alert(`이미지 사이즈: ${img.offsetWidth}x${img.offsetHeight}`);
  };
</script>

<img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0">
  • 위 예시에서 window.onload는 이미지가 모두 로드되고 난 후 실행되기 때문에 이미지 사이즈가 제대로 출력된다.

beforeunload

사용자가 페이지를 떠날 때 추가 확인을 요청할 수 있는데, 이는 beforeunload를 통해 제어해주면 된다.

// use addEventListener beforeunload
window.addEventListener("beforeunload", (event) => {
  // 표준에 따라 기본 동작 방지
  event.preventDefault();
  // Chrome에서는 returnValue 설정이 필요함
  event.returnValue = "";
});

  • 페이지를 벗어날때 나오던 위와 같은 알림 창이 바로 이런 상황에서 처리를 해준 것이다. 이벤트를 막아서 "정말 떠날 건지 확인"하는 경고창을 띄울 수 있다.
  • 표준(HTML Living Standard) 에서는 event.preventDefault() 호출만으로도 충분하다고 정의돼 있다. 하지만 크롬, 사파리, 엣지, 파이어폭스 등 주요 브라우저에서는 호환성을 위해 event.returnValue에 무언가를 할당해야 대화상자가 뜨도록 바뀌었다.

unload

unload 페이지가 완전히 종료될 때 마지막으로 할 수 있는 작업 이벤트이다.
페이지 종료 시 마지막 정리 작업이나 통계, 로그 전송 같은 목적에만 쓸 수 있다.
만약, 사용자와 상호작용(경고창 등)을 하려면 반드시 beforeunload를 써야 한다.

unload 이벤트는 사용자가 페이지를 떠날 때 발생하므로 unload 이벤트에서 분석 정보를 서버로 보낼 수도 있을 것이다.

세션 종료 로그 보내기

window.addEventListener("unload", () => {
  // 사용자가 페이지를 떠날 때 서버로 로그 전송
  navigator.sendBeacon("/log", JSON.stringify({ action: "exit", time: Date.now() }));
});

Analytics / 페이지 사용 통계 기록

window.addEventListener("unload", () => {
  // 페이지를 떠날 때 사용자가 머문 시간을 서버에 기록
  const timeSpent = Date.now() - performance.timing.navigationStart;
  navigator.sendBeacon("/analytics", JSON.stringify({ timeSpent }));
});
  • navigator.sendBeacon()를 쓰면 비동기 HTTP 요청을 안전하게 보낼 수 있다.
  • 일반 fetch나 XMLHttpRequest는 unload 시점에서는 대부분 취소된다.

⌨️ 자주 사용되는 이벤트(Event)

다양한 이벤트 타입이 있지만, 자주 사용되는 몇 가지를 중심으로 살펴보자.

마우스 클릭 이벤트

  • click : 요소를 클릭할 때 발생
  • dblclick : 더블 클릭할 떄 발생

키보드 이벤트

이벤트 객체를 인수로 받아, 어떤 키가 눌렸는지(event.key), 현재 이벤트 타입(event.type) 등 다양한 정보를 활용할 수 있다.

  • keyup: 누른 키에서 손을 뗄 때 실행
  • keydown: 키보드를 누를 때 실행. 키를 누르고 있을 때 단 한번만 실행
  • keypress(deprecated): 키보드를 누를 때 실행. 키를 누르고 있을 때 계속 실행

1️⃣ keyup

  • 언제? 키에서 손을 뗐을 때 실행
  • 특징
    - 입력이 끝났을 때를 감지할 수 있음
    - 입력 중에는 동작 안 하고, 손을 뗐을 때만 발생
  • 사용 예시
    - 검색창 자동완성 (keyup 시점에 서버 요청 보내기)
    - 입력 완료 후 유효성 검사 (비밀번호 길이, 이메일 포맷 체크 등)
    - 키 누르고 있다가 뗄 때 어떤 동작을 멈추는 상황 (게임 캐릭터 이동 중지)
  • 사용법
    - 핸들러는 이벤트객체 (event) 를 인수로 받음.
<body>
    <input id="text" type="text" />

    <script>
      const input = document.getElementById("text");
      input.addEventListener("keyup", (event) => {
        console.log("현재 입력값:", event, event.key);
      });
    </script>
  </body>
  • 결과값
    - "텍스트" : 입력한 특정 텍스트가 콘솔창에 출력됨
    - event : 해당 키보드 이벤트 관련된 모든 정보가 콘솔창에 출력됨
    - event.key : 오직 키보드 값만 콘솔창에 출력됨

2️⃣ keydown

  • 언제? 키를 "누르는 순간" 단 한 번 실행
  • 특징
    - 어떤 키든(문자, 화살표, F1~F12, ESC 등) 전부 인식 가능
    - 반복 입력(꾹 누르고 있을 때도 계속 발생)
  • 사용 예시
    - 단축키 구현 (Ctrl + S, ESC 눌렀을 때 동작 등)
    - 방향키로 캐릭터 움직이기
    - 입력 도중 특정 키 막기 (예: 숫자만 입력 허용)
document.addEventListener("keydown", (e) => {
  if (e.key === "Escape") {
    console.log("ESC 눌렀다!");
  }
});

3️⃣ keypress (⚠️ 현재는 표준에서 deprecated, 대부분 keydown으로 대체)

  • 언제? 키를 눌러서 실제 "문자"가 입력될 때 실행
  • 특징
    - 문자를 표현할 수 있는 키만 인식 (예: a, 1, ; 등)
    - 화살표키, F1 같은 제어 키는 감지 못함
    - 한글/이모지 같은 조합형 문자는 제대로 인식되지 않음
  • 사용 예시
    - 과거에는 "입력되는 문자 자체" 확인할 때 사용했음 (예: 텍스트 입력 감시)
    - 지금은 input 이벤트나 keydown을 대신 사용 권장

key 이벤트 발생 순서

키를 누르면 keydown 이벤트가 발생, 이어서 바로 keypress 이벤트가 발생. 그런 다음 키가 해제되면 keyup 이벤트가 생성된다.

💡 keydown > keypress > keyup 순으로 이벤트 진행

직접 코드로 확인: https://jsbin.com/vigimenaji/edit?html,output

포커스 이벤트(Input 창)

  • focus : 포커스가 맞춰질 때
  • blur : 포커스를 잃을 때
  • 사용자가 폼에 입력할 때 UI를 동적으로 변경할 수 있다.
 <body>
    <input id="text" type="text" />

    <script>
      const input = document.getElementById("text");
      input.addEventListener("focus", () => {
        input.style.backgroundColor = "lightblue";
      });

      input.addEventListener("blur", () => {
        input.style.backgroundColor = null;
      });
    </script>
  </body>

마우스 이벤트

  • mousemove : 마우스를 움직일 때마다 발생한다.
    • event.clientXevent.clientY를 사용하여 마우스 포인터의 현재 위치를 파악할 수 있다.
    • 이를 활용하여 웹 페이지에서 마우스 커서를 따라 움직이는 요소를 만들 수 있다.
 <body>
    <div
      id="box"
      style="
        position: relative;
        width: 100px;
        height: 100px;
        border: 2px solid lightblue;
      "
    ></div>

    <div
      id="circle"
      style="
        position: absolute;
        width: 10px;
        height: 10px;
        background-color: lightpink;
        border-radius: 50%;
      "
    ></div>

    <script>
      const box = document.getElementById("box");
      const circle = document.getElementById("circle");

      box.addEventListener("mousemove", (event) => {
        circle.style.top = `${event.clientY}px`;
        circle.style.left = `${event.clientX}px`;
      });
    </script>
  </body>

윈도우 이벤트

  • resize : 윈도우 창의 크기가 변경될 때 발생한다.
    • 오직 window 객체에서만 발생한다.
    • 이를 통해 창의 너비와 높이 값을 업데이트하여 반응형 웹 디자인에 활용할 수 있다.
 <script>
      window.addEventListener("resize", () => {
        document.body.innerText = `현재 창 크기는 ${window.innerWidth} x ${window.innerHeight}`;
      });
 </script>

값 변경 이벤트

  • input: 입력 필드의 값이 어떤 방식으로든 변경되었을 때마다 발생
    • 사용자가 키보드로 입력하는 것뿐만 아니라, 마우스로 붙여넣기, 음성 인식, 드래그 슬라이더 조작 등 다양한 방법으로 입력 필드(input, textarea, checkbox, radio, select 등)의 값이 달라졌을 때 트리거된다.
    • 따라서 실시간으로 입력 필드 값의 변화를 감지하여 즉각적으로 반응해야 하는 경우 유용하게 사용된다.
      • 실시간 유효성 검사: 사용자가 입력할 때마다 입력값의 유효성을 검사하여 오류 메시지를 즉시 보여줄 수 있다.
      • 값 변화 시 처리: 입력 필드에 텍스트를 입력하면 해당 텍스트를 다른 요소에 즉시 표시하거나, 특정 단어를 다른 것으로 자동으로 바꾸는 등의 처리를 할 수 있다.
<input type="text" id="input"> oninput: <span id="result"></span>
<script>
  input.oninput = function() {
    result.innerHTML = input.value;
  };
</script>
  • change: 입력 필드에서 포커스가 해제된 후에 값이 변경되었을 때 발생
    - change 이벤트는 input 이벤트와는 달리 HTML 요소가 포커스를 잃었을 때 사용자 입력이 종료되었다고 인식하여 발생한다.
<input type="text" onchange="alert(this.value)">
<input type="button" value="버튼">

즉, 사용자가 입력을 하고 있을 때는 input 이벤트가 발생하고, 사용자 입력이 종료되어 값이 변경되면 change 이벤트가 발생한다.

참고자료

2개의 댓글

comment-user-thumbnail
2025년 8월 29일

좋은 글이네요. 잘 봤습니다.

1개의 답글