본문 바로가기

Development/Javascript

모던 자바스크립트 입문 - Chapter 15: 이벤트 처리 💻

자바스크립트로 웹 개발을 하다 보면 사용자와의 상호작용이 필수적입니다.
버튼 클릭, 마우스 움직임, 키보드 입력 등 사용자의 모든 행동은 이벤트로 처리됩니다.
이번 포스트에서는 모던 자바스크립트에서 이벤트를 효과적으로 처리하는 방법들을 체계적으로 살펴보겠습니다.

📌 목차

  • 이벤트란 무엇인가?
  • 이벤트 리스너 등록하기
  • 이벤트 객체 활용하기
  • 이벤트 버블링와 캡처링
  • 이벤트 위임(Event Delegation)
  • 다양한 이벤트 타입들
  • 실무에서 자주 사용하는 패턴들

📝 이벤트란 무엇인가?

이벤트는 웹 페이지에서 발생하는 사용자의 행동이나 브라우저의 상태 변화를 의미합니다.

클릭, 키 입력, 페이지 로드 등이 모두 이벤트에 해당합니다.

// 가장 기본적인 이벤트 처리
document.getElementById('myButton').onclick = function() {
    alert('버튼이 클릭되었습니다!');
};

📝 이벤트 리스너 등록하기

addEventListener 메서드 사용

모던 자바스크립트에서는 addEventListener를 사용하는 것이 권장됩니다.

const button = document.getElementById('myButton');

// 기본 사용법
button.addEventListener('click', function() {
    console.log('버튼 클릭됨');
});

// 화살표 함수 사용
button.addEventListener('click', () => {
    console.log('화살표 함수로 처리');
});

// 함수를 별도로 정의
function handleClick() {
    console.log('별도 함수로 처리');
}
button.addEventListener('click', handleClick);

이벤트 리스너 제거하기

// 이벤트 리스너 제거
button.removeEventListener('click', handleClick);

// 한 번만 실행되는 이벤트
button.addEventListener('click', function() {
    console.log('한 번만 실행됩니다');
}, { once: true });

📝이벤트 객체 활용하기

이벤트가 발생할 때마다 이벤트 객체가 생성되어 핸들러 함수에 전달됩니다.

button.addEventListener('click', function(event) {
    console.log('이벤트 타입:', event.type);
    console.log('클릭된 요소:', event.target);
    console.log('현재 요소:', event.currentTarget);
    console.log('마우스 X좌표:', event.clientX);
    console.log('마우스 Y좌표:', event.clientY);
});

// 키보드 이벤트에서의 활용
document.addEventListener('keydown', function(event) {
    console.log('눌린 키:', event.key);
    console.log('키 코드:', event.keyCode);
    
    if (event.key === 'Enter') {
        console.log('엔터키가 눌렸습니다');
    }
});

📝이벤트 버블링과 캡처링

이벤트 버블링

이벤트가 발생한 요소에서 시작해서 부모 요소로 전파되는 현상입니다.

// HTML 구조: <div id="parent"><button id="child">클릭</button></div>

document.getElementById('parent').addEventListener('click', function() {
    console.log('부모 div 클릭됨');
});

document.getElementById('child').addEventListener('click', function() {
    console.log('자식 button 클릭됨');
});

// button 클릭 시 출력:
// "자식 button 클릭됨"
// "부모 div 클릭됨"

이벤트 전파 제어하기

document.getElementById('child').addEventListener('click', function(event) {
    console.log('자식 button 클릭됨');
    event.stopPropagation(); // 이벤트 전파 중단
});

// 기본 동작 방지
document.getElementById('myLink').addEventListener('click', function(event) {
    event.preventDefault(); // 링크의 기본 동작(페이지 이동) 방지
    console.log('링크 클릭됨');
});

이벤트 캡처링

// 캡처링 단계에서 이벤트 처리
document.getElementById('parent').addEventListener('click', function() {
    console.log('캡처링 단계에서 처리됨');
}, true); // 세 번째 매개변수를 true로 설정

📝이벤트 위임

부모 요소에서 자식 요소들의 이벤트를 처리하는 패턴입니다. 동적으로 생성되는 요소들에 특히 유용합니다.

// 전통적인 방식 (비효율적)
const buttons = document.querySelectorAll('.item-button');
buttons.forEach(button => {
    button.addEventListener('click', handleClick);
});

// 이벤트 위임 방식 (효율적)
document.getElementById('item-list').addEventListener('click', function(event) {
    if (event.target.classList.contains('item-button')) {
        handleClick(event);
    }
});

// 실용적인 예제
document.getElementById('todo-list').addEventListener('click', function(event) {
    const target = event.target;
    
    if (target.classList.contains('delete-btn')) {
        // 삭제 버튼 클릭 처리
        target.closest('.todo-item').remove();
    } else if (target.classList.contains('edit-btn')) {
        // 편집 버튼 클릭 처리
        editTodoItem(target.closest('.todo-item'));
    }
});

📝 다양한 이벤트 타입들

마우스 이벤트

const element = document.getElementById('myElement');

element.addEventListener('mouseenter', () => console.log('마우스 진입'));
element.addEventListener('mouseleave', () => console.log('마우스 떠남'));
element.addEventListener('mouseover', () => console.log('마우스 오버'));
element.addEventListener('mouseout', () => console.log('마우스 아웃'));
element.addEventListener('mousedown', () => console.log('마우스 버튼 누름'));
element.addEventListener('mouseup', () => console.log('마우스 버튼 뗌'));

키보드 이벤트

document.addEventListener('keydown', function(event) {
    switch(event.key) {
        case 'Escape':
            closeModal();
            break;
        case 'Enter':
            if (event.ctrlKey) {
                submitForm();
            }
            break;
        case 'ArrowUp':
            navigateUp();
            break;
    }
});

폼 이벤트

const form = document.getElementById('myForm');
const input = document.getElementById('myInput');

form.addEventListener('submit', function(event) {
    event.preventDefault(); // 폼 제출 방지
    
    const formData = new FormData(form);
    console.log('폼 데이터:', Object.fromEntries(formData));
});

input.addEventListener('focus', () => console.log('입력 필드 포커스'));
input.addEventListener('blur', () => console.log('입력 필드 포커스 해제'));
input.addEventListener('input', () => console.log('입력값 변경'));
input.addEventListener('change', () => console.log('입력값 확정'));

윈도우 이벤트

window.addEventListener('load', function() {
    console.log('페이지 완전히 로드됨');
});

window.addEventListener('resize', function() {
    console.log('윈도우 크기 변경됨');
});

window.addEventListener('scroll', function() {
    console.log('스크롤 위치:', window.scrollY);
});

📝 실무에서 자주 사용하는 패턴들

디바운싱(Debouncing)

연속적으로 발생하는 이벤트를 제어하는 기법입니다.

function debounce(func, delay) {
    let timeoutId;
    return function(...args) {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => func.apply(this, args), delay);
    };
}

// 검색 입력 필드에 적용
const searchInput = document.getElementById('search');
const debouncedSearch = debounce(function(event) {
    console.log('검색어:', event.target.value);
    // 실제 검색 로직 실행
}, 300);

searchInput.addEventListener('input', debouncedSearch);

스로틀링(Throttling)

function throttle(func, delay) {
    let lastCall = 0;
    return function(...args) {
        const now = new Date().getTime();
        if (now - lastCall < delay) {
            return;
        }
        lastCall = now;
        return func.apply(this, args);
    };
}

// 스크롤 이벤트에 적용
const throttledScroll = throttle(function() {
    console.log('스크롤 위치:', window.scrollY);
}, 100);

window.addEventListener('scroll', throttledScroll);

커스텀 이벤트

// 커스텀 이벤트 생성
const customEvent = new CustomEvent('myCustomEvent', {
    detail: { message: '커스텀 데이터' }
});

// 커스텀 이벤트 리스너 등록
document.addEventListener('myCustomEvent', function(event) {
    console.log('커스텀 이벤트 발생:', event.detail.message);
});

// 커스텀 이벤트 발생
document.dispatchEvent(customEvent);

이벤트 체이닝과 프로미스

function waitForClick(element) {
    return new Promise(resolve => {
        element.addEventListener('click', resolve, { once: true });
    });
}

// 사용 예
async function handleUserFlow() {
    console.log('버튼 클릭을 기다립니다...');
    await waitForClick(document.getElementById('nextButton'));
    console.log('다음 단계로 진행합니다!');
}

📚 마무리 정리

이번 포스트에서 배운 이벤트 처리 개념들을 정리하면:

기본 개념: 이벤트는 사용자 상호작용과 브라우저 상태 변화를 처리하는 핵심 메커니즘입니다.

이벤트 리스너: addEventListener를 사용하여 이벤트를 등록하고, 필요시 removeEventListener로 제거할 수 있습니다.

이벤트 객체: 이벤트 발생 시 생성되는 객체로, 이벤트에 대한 상세 정보를 제공합니다.

이벤트 전파: 버블링과 캡처링을 이해하고 stopPropagation()과 preventDefault()로 제어할 수 있습니다.

이벤트 위임: 부모 요소에서 자식 요소들의 이벤트를 처리하여 성능을 최적화할 수 있습니다.

실무 패턴: 디바운싱, 스로틀링, 커스텀 이벤트 등을 활용하여 더 나은 사용자 경험을 제공할 수 있습니다.

💡 오늘 새롭게 알게 된 것

  • 디바운싱과 스로틀링의 차이점과 각각의 적절한 사용 상황입니다
  • 커스텀 이벤트를 통해 컴포넌트 간 통신을 구현할 수 있다는 점입니다
  • once: true 옵션으로 일회성 이벤트 리스너를 간단히 만들 수 있다는 점입니다

🤔 어려웠던 점

  • 이벤트 버블링과 캡처링의 정확한 동작 순서를 이해하는게 어려웠습니다
  • 언제 이벤트 위임을 사용해야 하고 언제 직접 리스너를 등록해야 하는지 판단하는게 아직 어렵습니다
  • 디바운싱과 스로틀링 함수를 직접 구현할 때 클로저 개념을 정확히 적용하는 것이 어렵습니다

🎯 다음 학습 계획

다음 포스팅에서는 HTTP 제어(Ajax)에 대해 알아보겠습니다.