JavaScript 독학 가이드 8 - 이벤트 처리

learning by Seven Fingers Studio 17분
JavaScript이벤트이벤트리스너클릭웹개발프로그래밍

버튼 클릭하면 뭔가 일어나게 하고 싶죠? 마우스 올리면 색깔 바뀌고, 입력하면 실시간으로 검색 결과 나오고. 이 모든 게 이벤트로 가능합니다. 사용자의 모든 동작에 반응할 수 있어요.

이벤트가 뭔가요?

이벤트는 웹 페이지에서 일어나는 사건이에요. 클릭, 마우스 움직임, 키보드 입력 등 모든 게 이벤트입니다.

// "버튼이 클릭되면, 경고창을 띄워라"
button.addEventListener("click", function() {
    alert("버튼이 클릭됐어요!");
});

실행 결과:

버튼 클릭 시:

이 페이지에서 알림

버튼이 클릭됐어요!

이벤트 리스너 등록하기

addEventListener (추천!)

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

<script>
let btn = document.querySelector("#myBtn");

btn.addEventListener("click", function() {
    console.log("버튼 클릭됨!");
});
</script>

실행 결과:

> 버튼 클릭!
버튼 클릭됨!

여러 리스너를 추가할 수 있어요.

btn.addEventListener("click", function() {
    console.log("첫 번째 리스너");
});

btn.addEventListener("click", function() {
    console.log("두 번째 리스너");
});
// 둘 다 실행됨!

실행 결과:

> 버튼 클릭!
첫 번째 리스너
두 번째 리스너

두 리스너가 모두 순서대로 실행됩니다

on속성 (옛날 방식)

btn.onclick = function() {
    console.log("클릭!");
};

하나만 등록 가능하고, 나중에 덮어써집니다.

주요 이벤트 종류

마우스 이벤트

<button id="btn">마우스 테스트</button>

<script>
let btn = document.querySelector("#btn");

btn.addEventListener("click", () => console.log("클릭"));
btn.addEventListener("dblclick", () => console.log("더블클릭"));
btn.addEventListener("mouseenter", () => console.log("마우스 진입"));
btn.addEventListener("mouseleave", () => console.log("마우스 나감"));
btn.addEventListener("mousedown", () => console.log("마우스 버튼 누름"));
btn.addEventListener("mouseup", () => console.log("마우스 버튼 뺌"));
</script>

실행 결과:

> 마우스를 버튼 위로 올리면...
마우스 진입
> 마우스 버튼을 누르면...
마우스 버튼 누름
> 마우스 버튼을 떼면...
마우스 버튼 뗌
클릭
> 빠르게 두 번 클릭하면...
더블클릭
> 마우스를 밖으로 이동하면...
마우스 나감

키보드 이벤트

<input type="text" id="myInput">

<script>
let input = document.querySelector("#myInput");

input.addEventListener("keydown", () => console.log("키 누름"));
input.addEventListener("keyup", () => console.log("키 뗌"));
input.addEventListener("keypress", () => console.log("키 입력")); // 거의 안 씀
</script>

실행 결과:

> 'A' 키를 누르면...
키 누름
키 입력
> 'A' 키를 떼면...
키 뗌

폼 이벤트

<form id="myForm">
    <input type="text" id="username">
    <button type="submit">제출</button>
</form>

<script>
let form = document.querySelector("#myForm");
let username = document.querySelector("#username");

username.addEventListener("focus", () => console.log("포커스"));
username.addEventListener("blur", () => console.log("포커스 해제"));
username.addEventListener("input", () => console.log("입력 중"));
username.addEventListener("change", () => console.log("값 변경됨"));

form.addEventListener("submit", (e) => {
    e.preventDefault(); // 폼 제출 막기
    console.log("폼 제출!");
});
</script>

실행 결과:

> 입력창 클릭하면...
포커스
> "홍길동" 입력하면...
입력 중
입력 중
입력 중
> 다른 곳 클릭하면...
포커스 해제
값 변경됨
> 제출 버튼 클릭하면...
폼 제출!

preventDefault()로 페이지 새로고침을 막았습니다

기타 이벤트

// 페이지 로드 완료
window.addEventListener("load", () => {
    console.log("페이지 로드됨");
});

// 스크롤
window.addEventListener("scroll", () => {
    console.log("스크롤 중");
});

// 리사이즈
window.addEventListener("resize", () => {
    console.log("창 크기 변경");
});

이벤트 객체

이벤트가 발생하면 이벤트 정보를 담은 객체가 전달돼요.

btn.addEventListener("click", function(event) {
    console.log(event); // 이벤트 정보
    console.log(event.type); // "click"
    console.log(event.target); // 클릭된 요소
});

실행 결과:

> console.log(event)
MouseEvent {type: "click", target: button#myBtn, ...}
> console.log(event.type)
click
> console.log(event.target)
<button id="myBtn">클릭</button>

마우스 위치

document.addEventListener("click", function(e) {
    console.log(`X: ${e.clientX}, Y: ${e.clientY}`);
});

실행 결과:

> 화면 왼쪽 위를 클릭하면...
X: 45, Y: 120
> 화면 중앙을 클릭하면...
X: 640, Y: 400
> 화면 오른쪽 아래를 클릭하면...
X: 1200, Y: 780

clientX, clientY는 브라우저 창 기준 마우스 좌표입니다

눌린 키 확인

document.addEventListener("keydown", function(e) {
    console.log(`눌린 키: ${e.key}`);
    console.log(`키 코드: ${e.code}`);
});

실행 결과:

> 'A' 키를 누르면...
눌린 키: a
키 코드: KeyA
> Enter 키를 누르면...
눌린 키: Enter
키 코드: Enter
> Shift 키를 누르면...
눌린 키: Shift
키 코드: ShiftLeft

이벤트 버블링과 캡처링

버블링

이벤트가 자식에서 부모로 전파돼요.

<div id="outer">
    <div id="inner">
        <button id="btn">클릭</button>
    </div>
</div>

<script>
document.querySelector("#btn").addEventListener("click", () => {
    console.log("버튼 클릭");
});

document.querySelector("#inner").addEventListener("click", () => {
    console.log("inner 클릭");
});

document.querySelector("#outer").addEventListener("click", () => {
    console.log("outer 클릭");
});

// 버튼 클릭 시 출력:
// 버튼 클릭
// inner 클릭
// outer 클릭
</script>

실행 결과:

> 버튼 클릭 시...
버튼 클릭
inner 클릭
outer 클릭

이벤트가 자식 → 부모 방향으로 버블링됩니다

버블링 막기

btn.addEventListener("click", function(e) {
    e.stopPropagation(); // 부모로 전파 안 됨
    console.log("버튼만 클릭");
});

실행 결과:

> stopPropagation() 사용 시...
버튼만 클릭
(inner 클릭과 outer 클릭은 실행되지 않음)

버블링이 중단되어 부모 요소의 이벤트가 실행되지 않습니다

이벤트 위임

부모에 리스너를 하나만 달고, 자식들을 처리하는 기법이에요.

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

<script>
// 나쁜 예: 각 li에 리스너 추가
// let items = document.querySelectorAll("li");
// items.forEach(item => {
//     item.addEventListener("click", function() {
//         console.log(this.textContent);
//     });
// });

// 좋은 예: 부모(ul)에 하나만 추가
let list = document.querySelector("#list");
list.addEventListener("click", function(e) {
    if (e.target.tagName === "LI") {
        console.log(e.target.textContent);
    }
});
</script>

실행 결과:

> "항목 1" 클릭 시...
항목 1
> "항목 2" 클릭 시...
항목 2
> "항목 3" 클릭 시...
항목 3

부모 요소 하나에만 리스너를 등록해도 모든 자식 요소의 클릭을 처리할 수 있습니다

동적으로 추가된 요소에도 작동해요!

실전 예제

1. 카운터

<!DOCTYPE html>
<html>
<body>
    <h1 id="count">0</h1>
    <button id="increaseBtn">+1</button>
    <button id="decreaseBtn">-1</button>
    <button id="resetBtn">초기화</button>

    <script>
        let count = 0;
        let display = document.querySelector("#count");

        document.querySelector("#increaseBtn").addEventListener("click", () => {
            count++;
            display.textContent = count;
        });

        document.querySelector("#decreaseBtn").addEventListener("click", () => {
            count--;
            display.textContent = count;
        });

        document.querySelector("#resetBtn").addEventListener("click", () => {
            count = 0;
            display.textContent = count;
        });
    </script>
</body>
</html>

실행 결과:

5

> +1 버튼 5번 클릭...
화면에 표시: 0 → 1 → 2 → 3 → 4 → 5

2. 실시간 검색

<!DOCTYPE html>
<html>
<body>
    <input type="text" id="searchInput" placeholder="검색어 입력">
    <p id="result"></p>

    <script>
        let input = document.querySelector("#searchInput");
        let result = document.querySelector("#result");

        input.addEventListener("input", function() {
            let text = input.value;
            result.textContent = `검색어: ${text}`;
        });
    </script>
</body>
</html>

실행 결과:

검색어: 자바스크립트

> "자" 입력 시...
검색어: 자
> "바" 추가 입력 시...
검색어: 자바
> 계속 입력하면...
검색어: 자바스크립트

input 이벤트로 타이핑할 때마다 실시간으로 반응합니다

3. To-Do 리스트

<!DOCTYPE html>
<html>
<body>
    <input type="text" id="todoInput">
    <button id="addBtn">추가</button>
    <ul id="todoList"></ul>

    <script>
        let input = document.querySelector("#todoInput");
        let addBtn = document.querySelector("#addBtn");
        let list = document.querySelector("#todoList");

        addBtn.addEventListener("click", addTodo);
        input.addEventListener("keypress", function(e) {
            if (e.key === "Enter") {
                addTodo();
            }
        });

        function addTodo() {
            let text = input.value.trim();
            if (!text) return;

            let li = document.createElement("li");
            li.textContent = text;

            let deleteBtn = document.createElement("button");
            deleteBtn.textContent = "삭제";
            deleteBtn.addEventListener("click", function() {
                li.remove();
            });

            li.appendChild(deleteBtn);
            list.appendChild(li);
            input.value = "";
        }
    </script>
</body>
</html>

실행 결과:

  • 장보기
  • 운동하기
  • 책 읽기

Enter 키를 누르거나 추가 버튼을 클릭하면 새 항목이 추가됩니다

4. 모달 창

<!DOCTYPE html>
<html>
<head>
    <style>
        .modal {
            display: none;
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 20px;
            border: 2px solid #333;
        }
        .modal.active {
            display: block;
        }
        .overlay {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.5);
        }
        .overlay.active {
            display: block;
        }
    </style>
</head>
<body>
    <button id="openBtn">모달 열기</button>

    <div class="overlay" id="overlay"></div>
    <div class="modal" id="modal">
        <h2>모달 창</h2>
        <p>내용입니다</p>
        <button id="closeBtn">닫기</button>
    </div>

    <script>
        let openBtn = document.querySelector("#openBtn");
        let closeBtn = document.querySelector("#closeBtn");
        let modal = document.querySelector("#modal");
        let overlay = document.querySelector("#overlay");

        openBtn.addEventListener("click", () => {
            modal.classList.add("active");
            overlay.classList.add("active");
        });

        closeBtn.addEventListener("click", closeModal);
        overlay.addEventListener("click", closeModal);

        function closeModal() {
            modal.classList.remove("active");
            overlay.classList.remove("active");
        }
    </script>
</body>
</html>

실행 결과:

모달 창

내용입니다

"모달 열기" 버튼 클릭 시 오버레이와 모달이 나타나고, "닫기" 버튼이나 오버레이 클릭 시 사라집니다

자주 하는 실수

1. 이벤트 리스너를 반복문에서 잘못 등록

// 틀림 (var 사용 시)
for (var i = 0; i < 3; i++) {
    let btn = document.createElement("button");
    btn.textContent = i;
    btn.addEventListener("click", function() {
        console.log(i); // 항상 3 출력됨
    });
    document.body.appendChild(btn);
}

// 맞음: let 사용
for (let i = 0; i < 3; i++) {
    let btn = document.createElement("button");
    btn.textContent = i;
    btn.addEventListener("click", function() {
        console.log(i); // 0, 1, 2 정상 출력
    });
    document.body.appendChild(btn);
}

실행 결과:

❌ var 사용 시 (잘못된 예):
> 버튼 0 클릭 → 3
> 버튼 1 클릭 → 3
> 버튼 2 클릭 → 3
✓ let 사용 시 (올바른 예):
> 버튼 0 클릭 → 0
> 버튼 1 클릭 → 1
> 버튼 2 클릭 → 2

var는 함수 스코프, let은 블록 스코프이므로 반복문에서는 let을 사용해야 합니다

2. preventDefault 잘못 쓰기

// form 제출 막을 때
form.addEventListener("submit", function(e) {
    e.preventDefault(); // 맨 위에 써야 함
    // 폼 처리 로직
});

3. this 헷갈림

btn.addEventListener("click", function() {
    console.log(this); // btn (이벤트가 발생한 요소)
});

btn.addEventListener("click", () => {
    console.log(this); // window (화살표 함수는 this 바인딩 안 됨)
});

실행 결과:

일반 함수 사용 시:
> console.log(this)
<button id="myBtn">클릭</button>
화살표 함수 사용 시:
> console.log(this)
Window {document: Document, ...}

이벤트 핸들러에서 this를 사용하려면 일반 함수를 사용해야 합니다

다음 단계

다음 글에서는 ES6+ 최신 문법을 배웁니다. 구조 분해, 스프레드 연산자, async/await 등 모던 JavaScript 문법을 알아볼 거예요.

이벤트 처리를 마스터하면 진짜 인터랙티브한 웹사이트를 만들 수 있어요!


JavaScript 독학 가이드 시리즈:

← 블로그 목록으로