frontend/javascript

🟨 2-1. 자바스크립트 이벤트 시스템의 모든 것

mirabo01 2025. 11. 6. 22:15

1. 왜 이벤트를 먼저 이해해야 할까

웹 페이지는 결국 “사용자 입력에 반응하는 프로그램”이다.

  • 버튼을 클릭했을 때
  • 스크롤했을 때
  • 키보드를 눌렀을 때
  • 폼을 전송하려 할 때

이 모든 순간이 이벤트(event) 다.
자바스크립트는 이 이벤트를 “듣고(listen)” 있다가, 발생하면 특정 함수를 실행한다.

이 글에서는 단순히 onclick 예제를 넘어서:

  • 이벤트를 등록하는 여러 가지 방식
  • addEventListener가 표준인 이유
  • 이벤트 객체(event object)의 역할
  • 버블링/캡처, stopPropagation, preventDefault
  • 이벤트 위임(Event Delegation) 패턴

까지 한 번에 정리한다.


2. 이벤트란 무엇인가

이벤트는 쉽게 말해

“브라우저에서 일어난 사건을 자바스크립트가 감지할 수 있게 표현한 것”

주요 예시는 이런 것들이다.

  • click : 클릭
  • input : 입력 필드 값이 바뀔 때
  • submit : form 제출
  • keydown, keyup : 키보드 입력
  • scroll : 스크롤 발생
  • change : select, checkbox 값 변경

브라우저는 이런 이벤트가 일어날 때마다
해당 이벤트에 “등록된 함수(이벤트 핸들러)”를 불러준다.


3. 이벤트 등록 방식 3가지

이벤트를 다는 방법은 크게 세 가지가 있다.

3-1. HTML 속성에 직접 쓰기 (비추천)

<button onclick="handleClick()">클릭</button>

<script>
  function handleClick() {
    alert("버튼 클릭!");
  }
</script>

직관적으로 보이지만 단점이 많다.

  • HTML과 JS가 뒤섞여서 유지보수가 힘들고
  • 인라인으로 로직이 늘어나기 쉽고
  • 같은 요소에 여러 개의 핸들러를 붙이기 어렵다

실무에서는 거의 쓰지 않는 방식이라고 보면 된다.


3-2. 요소 속성에 함수 대입하기

<button id="btn">클릭</button>

<script>
  const btn = document.getElementById("btn");
  btn.onclick = function () {
    alert("클릭!");
  };
</script>

이 방식은 인라인보다는 낫지만,
onclick 속성에는 하나의 함수만 대입할 수 있다.

btn.onclick = function () {
  console.log("첫 번째");
};
btn.onclick = function () {
  console.log("두 번째");
};
// 마지막 것만 실행됨

기존 핸들러를 덮어쓰기 때문에 확장성 측면에서 아쉽다.


3-3. addEventListener 사용하기 (표준)

btn.addEventListener("click", function () {
  console.log("첫 번째 클릭 핸들러");
});

btn.addEventListener("click", function () {
  console.log("두 번째 클릭 핸들러");
});

addEventListener 의 장점:

  • 같은 이벤트에 핸들러를 여러 개 붙일 수 있고
  • 나중에 removeEventListener로 제거 가능
  • 캡처/버블링 옵션 등 세밀한 제어 가능

실무에서는 무조건 이 방식만 쓴다고 생각해도 된다.


4. 이벤트 핸들러와 이벤트 객체

이벤트가 발생하면 브라우저는 이벤트 핸들러를 호출할 때
자동으로 이벤트 객체(event object) 를 넘겨준다.

btn.addEventListener("click", function (event) {
  console.log("이벤트 타입:", event.type); // click
  console.log("이벤트가 발생한 요소:", event.target);
});

자주 쓰는 프로퍼티 몇 가지만 정리해보면:

  • event.type : 이벤트 종류 ("click", "input" 등)
  • event.target : 이벤트가 실제로 발생한 요소
  • event.currentTarget : 핸들러가 걸려 있는 요소
  • event.clientX, event.clientY : 마우스 좌표
  • event.key, event.code : 키보드 입력 정보
  • event.preventDefault() : 기본 동작 막기
  • event.stopPropagation() : 전파(버블링/캡처) 막기

특히 target vs currentTarget 은
이벤트 위임을 이해할 때 아주 중요하다.


5. this, event.target, event.currentTarget 차이

많이 헷갈리는 부분이라 예제로 한 번 정리해보자.

<ul id="list">
  <li>첫 번째</li>
  <li>두 번째</li>
  <li>세 번째</li>
</ul>
const list = document.getElementById("list");

list.addEventListener("click", function (event) {
  console.log("this:", this);                  // list (ul)
  console.log("currentTarget:", event.currentTarget); // list (ul)
  console.log("target:", event.target);        // 실제 클릭한 li
});
  • this : 전통 함수에서 핸들러가 걸린 요소를 가리킴 (ul)
  • event.currentTarget : 항상 핸들러가 걸려 있는 요소 (ul)
  • event.target : 실제로 이벤트가 발생한(클릭된) 요소 (li)

이 차이는 잠시 후에 나올 이벤트 버블링/위임 과 연결된다.


6. 이벤트 버블링(Bubbling)과 캡처링(Capturing)

이제 자바스크립트 이벤트의 진짜 핵심인
이벤트 전파(Event Propagation) 개념을 보자.

브라우저에서 클릭 같은 이벤트가 발생하면,
이벤트는 다음 순서로 전파된다.

  1. 가장 상위 요소에서 시작 → 자식으로 내려가는 캡처링 단계
  2. 실제 요소에서 이벤트 발생
  3. 다시 부모 방향으로 올라가는 버블링 단계

기본적으로 우리가 쓰는 addEventListener("click", handler)는
버블링 단계에서 이벤트를 듣는다.


6-1. 간단한 예로 전파 흐름 보기

<div id="outer" style="padding:20px; background:#eee;">
  OUTER
  <div id="inner" style="padding:20px; background:#ccc;">
    INNER
    <button id="btn">버튼</button>
  </div>
</div>
const outer = document.getElementById("outer");
const inner = document.getElementById("inner");
const btn = document.getElementById("btn");

outer.addEventListener("click", () => console.log("OUTER"));
inner.addEventListener("click", () => console.log("INNER"));
btn.addEventListener("click", () => console.log("BUTTON"));

버튼을 클릭하면 콘솔에는 다음 순서로 출력된다.

BUTTON
INNER
OUTER

이게 버블링(Bubbling) 이다.
가장 안쪽 요소에서부터 바깥으로 “거품처럼” 올라가는 구조다.


6-2. 캡처링 단계에서 이벤트 받기

캡처링 단계에서 이벤트를 듣고 싶다면
addEventListener 의 세 번째 인자로 { capture: true } 를 넘긴다.

outer.addEventListener(
  "click",
  () => console.log("OUTER CAPTURE"),
  { capture: true }
);

버튼을 클릭하면:

  1. OUTER CAPTURE (캡처 단계)
  2. BUTTON
  3. INNER
  4. OUTER (버블링 단계)

이 순서로 출력된다.

일반적으로는 버블링만 잘 이해해도 충분하고,
캡처는 특별한 경우에만 사용한다.


7. 이벤트 전파 막기 — stopPropagation()

버블링 때문에 의도치 않게 부모 핸들러까지 실행되는 경우가 있다.

<div id="card">
  <button id="deleteBtn">삭제</button>
</div>
const card = document.getElementById("card");
const deleteBtn = document.getElementById("deleteBtn");

card.addEventListener("click", () => {
  console.log("카드 클릭됨");
});

deleteBtn.addEventListener("click", (event) => {
  console.log("삭제 버튼 클릭됨");
  event.stopPropagation(); // 여기서 전파 차단
});

stopPropagation() 을 호출하면,
해당 이벤트는 더 이상 부모로 전파되지 않는다.

즉, 삭제 버튼을 눌러도 card 의 클릭 핸들러는 실행되지 않는다.


8. 기본 동작 막기 — preventDefault()

일부 요소는 “기본 동작”이 있다.

  • a 태그 클릭 → 링크 이동
  • form 제출 → 페이지 새로고침
  • 체크박스 클릭 → 체크 상태 변경

이 기본 동작을 막고 싶을 때 event.preventDefault() 를 쓴다.

네이버로 이동
const link = document.getElementById("link");
link.addEventListener("click", (event) => {
  event.preventDefault();
  console.log("이동 막고, 우리 로직 실행");
});

폼에서도 자주 쓰인다.

form.addEventListener("submit", (event) => {
  event.preventDefault(); // 새로고침 방지
  // 폼 값 검사 후 AJAX로 전송
});

9. 실무 핵심: 이벤트 위임(Event Delegation)

리스트 항목이 동적으로 늘어나는 UI에서는
각 항목마다 일일이 이벤트를 걸면 비효율적이다.

<ul id="todoList">
  <li>할 일 1</li>
  <li>할 일 2</li>
</ul>

나쁜 방식 (각 li에 직접 이벤트를 다는 경우)

const items = document.querySelectorAll("#todoList li");
items.forEach((item) => {
  item.addEventListener("click", () => {
    console.log(item.textContent);
  });
});

문제:

  • 새로운 li가 나중에 추가되면, 다시 이벤트를 붙여줘야 함
  • 요소가 수백 개, 수천 개가 되면 성능 부담

좋은 방식: 부모 하나에만 이벤트를 달고, event.target 으로 분기

const list = document.getElementById("todoList");

list.addEventListener("click", (event) => {
  if (event.target.tagName !== "LI") return;

  console.log("클릭한 항목:", event.target.textContent);
});

이 패턴을 이벤트 위임(Event Delegation) 이라고 한다.

  • 부모 요소 하나에만 핸들러를 등록
  • 실제 클릭된 자식 요소는 event.target 으로 판별
  • 동적으로 추가된 항목까지 자동으로 처리 가능

실제 서비스에서 리스트, 테이블, 메뉴 등은
거의 전부 이런 방식으로 구현한다고 보면 된다.


10. 키보드와 입력 이벤트 예시

이벤트는 클릭만 있는 게 아니다.
키보드, 입력 필드도 자주 다루게 된다.

<input id="search" placeholder="검색어 입력" />
const searchInput = document.getElementById("search");

searchInput.addEventListener("input", (event) => {
  console.log("현재 값:", event.target.value);
});

searchInput.addEventListener("keydown", (event) => {
  if (event.key === "Enter") {
    console.log("엔터 입력 → 검색 실행");
  }
});
  • input : 값이 변경될 때마다 발생 (실시간 반응)
  • keydown : 키를 눌렀을 때
  • keyup : 키에서 손을 뗐을 때

검색창 자동완성, 실시간 필터, 폼 검증 등에서 자주 쓰이는 패턴이다.


11. 자주 하는 실수 정리

  1. onclick와 addEventListener 혼용
    → 한쪽만 사용하자. 가급적 addEventListener 위주로.
  2. this와 event.target 혼동
    → 핸들러가 어디에 걸려 있는지, 실제로 클릭한 요소가 무엇인지 구분해야 한다.
  3. 버블링을 고려하지 않고 중첩 요소에 핸들러 등록
    → 의도치 않게 부모 핸들러까지 실행되는 경우. 필요하면 stopPropagation() 사용.
  4. 동적으로 생성되는 요소에 직접 이벤트를 붙이려는 경우
    → 이벤트 위임으로 해결하는 습관을 들이자.

12. 마무리

이벤트 시스템은 프론트엔드의 “신경 시스템”에 가깝다.

  • 어떤 일이 일어났는지 감지하고
  • 그에 따라 어떤 로직을 실행할지 결정하며
  • 여러 요소와 로직을 하나의 흐름으로 엮어준다

이번 글에서 다룬 핵심은 다음과 같다.

  • 이벤트 등록: addEventListener 를 기준으로 생각하기
  • 이벤트 객체: event.target, event.currentTarget, preventDefault, stopPropagation
  • 전파: 버블링 / 캡처 구조 이해
  • 이벤트 위임: 부모에만 이벤트 걸고 자식은 event.target 으로 처리

이 정도까지 이해하면,
버튼 클릭부터, 동적 리스트, 폼 검증, 모달, 드롭다운, 탭, 아코디언까지
대부분의 인터랙션을 구현할 수 있다.