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) 개념을 보자.
브라우저에서 클릭 같은 이벤트가 발생하면,
이벤트는 다음 순서로 전파된다.
- 가장 상위 요소에서 시작 → 자식으로 내려가는 캡처링 단계
- 실제 요소에서 이벤트 발생
- 다시 부모 방향으로 올라가는 버블링 단계
기본적으로 우리가 쓰는 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 }
);
버튼을 클릭하면:
- OUTER CAPTURE (캡처 단계)
- BUTTON
- INNER
- 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. 자주 하는 실수 정리
- onclick와 addEventListener 혼용
→ 한쪽만 사용하자. 가급적 addEventListener 위주로. - this와 event.target 혼동
→ 핸들러가 어디에 걸려 있는지, 실제로 클릭한 요소가 무엇인지 구분해야 한다. - 버블링을 고려하지 않고 중첩 요소에 핸들러 등록
→ 의도치 않게 부모 핸들러까지 실행되는 경우. 필요하면 stopPropagation() 사용. - 동적으로 생성되는 요소에 직접 이벤트를 붙이려는 경우
→ 이벤트 위임으로 해결하는 습관을 들이자.
12. 마무리
이벤트 시스템은 프론트엔드의 “신경 시스템”에 가깝다.
- 어떤 일이 일어났는지 감지하고
- 그에 따라 어떤 로직을 실행할지 결정하며
- 여러 요소와 로직을 하나의 흐름으로 엮어준다
이번 글에서 다룬 핵심은 다음과 같다.
- 이벤트 등록: addEventListener 를 기준으로 생각하기
- 이벤트 객체: event.target, event.currentTarget, preventDefault, stopPropagation
- 전파: 버블링 / 캡처 구조 이해
- 이벤트 위임: 부모에만 이벤트 걸고 자식은 event.target 으로 처리
이 정도까지 이해하면,
버튼 클릭부터, 동적 리스트, 폼 검증, 모달, 드롭다운, 탭, 아코디언까지
대부분의 인터랙션을 구현할 수 있다.
'frontend > javascript' 카테고리의 다른 글
| 🟨 2-3. 폼(Form)과 유효성 검사(Validation) — submit, input, change 이벤트로 배우는 입력 검증의 모든 것 (0) | 2025.11.07 |
|---|---|
| 🟨 2-2. 실전 이벤트 패턴 — 모달, 드롭다운, 아코디언, 탭 메뉴 한 번에 완성하기 (0) | 2025.11.07 |
| 🟨 1-16. ES6 모듈 + 비동기 + 클래스 기반으로 미니 프로젝트 만들기 (User CRUD 실습) (0) | 2025.11.06 |
| 🟨 1-15. 모듈화 + 객체지향 설계로 프로젝트 구조 설계하기 — 폴더 구조부터 설계 철학까지 (0) | 2025.11.06 |
| 🟨 1-14. 객체지향 설계 심화 — 캡슐화, 상속, 추상화, 다형성을 자바스크립트로 구현하기 (0) | 2025.11.06 |