jQuery event 다루기

예제 기준 · jQuery : v3.7.1

버튼 클릭, 선택값 변경, 문자 입력은 발생 시점이 서로 다릅니다. click·change·input부터 .on()과 .off(), 동적 요소의 이벤트 위임, preventDefault(), 버블링과 stopPropagation()까지 필요한 코드를 살펴보겠습니다.

각 절은 독립된 예제입니다. HTML 요소가 만들어진 뒤 실행하며 jQuery 코드는 jQuery가 로드된 화면을 기준으로 합니다. 결과는 pre#result에 표시합니다.

click 이벤트 처리

‘클릭하기’ 버튼 #target을 누를 때마다 횟수를 증가시킵니다. 결과를 갱신하는 함수와 이벤트 핸들러를 분리하면 초기 표시에도 같은 함수를 사용할 수 있습니다.

jQuery

let count = 0;
function updateState() {
    $('#result').text('click: ' + count + '회');
}
$('#target').on('click', function () {
    count += 1;
    updateState();
});
updateState();

JavaScript

let count = 0;
function updateState() {
    document.querySelector('#result').textContent = 'click: ' + count + '회';
}
document.querySelector('#target').addEventListener('click', function () {
    count += 1;
    updateState();
});
updateState();

click은 마우스로 누를 때뿐 아니라 버튼을 키보드로 활성화할 때도 발생합니다. 아래의 ‘초기화’는 클릭 횟수를 0으로 되돌리며 등록된 이벤트는 유지합니다. 본문 코드는 핵심 이벤트 처리를 중심으로 제시하고 초기화 버튼 연결은 생략했습니다.

핸들러의 인자로 받는 event에는 이벤트 종류인 type, 발생한 요소인 target 등의 정보가 들어 있습니다. JavaScript는 브라우저의 Event 계열 객체를 받고, jQuery는 공통 속성과 메서드를 제공하는 jQuery 이벤트 객체를 전달합니다. 브라우저에서 전달된 원래 이벤트는 jQuery의 originalEvent로 접근할 수 있습니다.

change 이벤트 처리

과일 select #target에는 ‘선택’(빈 값), Apple(apple), Banana(banana)이 있습니다. 다른 값을 선택하면 change 횟수와 현재 value를 표시합니다.

jQuery

let count = 0;
$('#target').on('change', function () {
    count += 1;
    $('#result').text('change: ' + count + '회 | value=' + $(this).val());
});
$('#result').text('change: 0회 | value=');

JavaScript

let count = 0;
document.querySelector('#target').addEventListener('change', function (event) {
    count += 1;
    document.querySelector('#result').textContent =
        'change: ' + count + '회 | value=' + event.currentTarget.value;
});
document.querySelector('#result').textContent = 'change: 0회 | value=';

같은 값을 다시 선택해도 새로운 변경은 생기지 않습니다. select는 사용자가 선택을 바꿀 때, 문자 input은 값을 바꾸고 초점을 벗어나는 등 변경을 확정할 때 change가 발생합니다. 입력 도중의 변화가 필요하면 다음 절의 input을 사용합니다.

코드로 .val(...)이나 value를 대입하는 것만으로 change는 발생하지 않습니다. 아래의 초기화도 선택값과 횟수를 직접 바꾸고 결과를 다시 표시합니다.

input 이벤트 처리

문자 입력란 #target의 값을 바꿀 때마다 input이 발생합니다. 현재 입력값, input 횟수, change 횟수, change 시점에 확정한 값을 함께 표시하면 두 이벤트의 차이를 확인할 수 있습니다.

jQuery

let inputCount = 0;
let changeCount = 0;
let committed = '';
function updateState() {
    $('#result').text('현재 입력: ' + $('#target').val()
        + '\ninput: ' + inputCount + '회\nchange: ' + changeCount
        + '회\nchange 시점 값: ' + committed);
}
$('#target').on('input', function () {
    inputCount += 1;
    updateState();
});
$('#target').on('change', function () {
    changeCount += 1;
    committed = $(this).val();
    updateState();
});
updateState();

JavaScript

const target = document.querySelector('#target');
let inputCount = 0;
let changeCount = 0;
let committed = '';
function updateState() {
    document.querySelector('#result').textContent = '현재 입력: ' + target.value
        + '\ninput: ' + inputCount + '회\nchange: ' + changeCount
        + '회\nchange 시점 값: ' + committed;
}
target.addEventListener('input', function () {
    inputCount += 1;
    updateState();
});
target.addEventListener('change', function () {
    changeCount += 1;
    committed = target.value;
    updateState();
});
updateState();

입력·삭제·붙여넣기로 값이 달라지면 input 결과가 갱신됩니다. 문자 입력 후 Tab 키나 ‘초점 이동’ 버튼으로 입력란의 초점을 벗어나면 change 시점 값도 갱신됩니다. 값이 바뀌지 않은 채 초점만 이동하면 change가 새로 발생하지 않습니다.

input 횟수는 키를 누른 횟수나 글자 수와 같다고 볼 수 없습니다. 한글처럼 조합하는 입력도 있기 때문입니다. 코드로 value를 대입하는 것은 input을 자동 발생시키지 않습니다. 아래 초기화는 입력값·두 횟수·확정값을 함께 비웁니다.

.on()으로 여러 요소에 이벤트 등록하기

Test 1·Test 2·Test 3 버튼은 모두 .sample class를 사용합니다. 같은 핸들러를 연결하고 클릭한 버튼의 이름과 전체 합계를 표시합니다.

jQuery

let count = 0;
function handleClick() {
    count += 1;
    $('#result').text($(this).text() + ' 클릭 | 합계 ' + count + '회');
}
$('.sample').on('click.demo', handleClick);
$('#result').text('합계 0회');

JavaScript

let count = 0;
function handleClick(event) {
    count += 1;
    document.querySelector('#result').textContent =
        event.currentTarget.textContent + ' 클릭 | 합계 ' + count + '회';
}
const buttons = document.querySelectorAll('.sample');
for (let i = 0; i < buttons.length; i++) {
    buttons[i].addEventListener('click', handleClick);
}
document.querySelector('#result').textContent = '합계 0회';

.on(이벤트, 핸들러)는 조회된 요소들에 함수를 연결합니다. 위 코드의 click.demo에서 click은 이벤트 종류, demo는 jQuery가 등록·제거 범위를 구분할 때 사용하는 네임스페이스입니다. 네이티브 addEventListener()에는 같은 네임스페이스 기능이 없으므로 이벤트 이름은 click을 사용합니다.

일반 function으로 등록한 jQuery 직접 핸들러의 this는 연결된 버튼입니다. JavaScript의 currentTarget도 여기서는 연결된 버튼이며, target은 버튼 안쪽의 아이콘처럼 실제 클릭된 요소일 수 있습니다. HTML 들여쓰기 공백까지 읽힐 수 있으므로 버튼 이름을 정돈해서 표시해야 한다면 읽은 텍스트에 trim()을 적용할 수 있습니다.

.off()로 이벤트 제거하기

처음에는 ‘테스트 클릭’ 버튼의 핸들러가 등록되지 않은 상태입니다. ‘이벤트 등록’ #register와 ‘이벤트 제거’ #remove로 동작을 바꾸고 등록 여부와 처리 횟수를 확인합니다.

jQuery

const target = $('#target');
let count = 0;
let active = false;
function updateState() {
    $('#result').text('이벤트 등록: ' + active + '\n처리 횟수: ' + count);
}
function handleClick() {
    count += 1;
    updateState();
}
$('#register').on('click', function () {
    target.off('click.demo', handleClick);
    target.on('click.demo', handleClick);
    active = true;
    updateState();
});
$('#remove').on('click', function () {
    target.off('click.demo', handleClick);
    active = false;
    updateState();
});
updateState();

JavaScript

const target = document.querySelector('#target');
let count = 0;
let active = false;
function updateState() {
    document.querySelector('#result').textContent =
        '이벤트 등록: ' + active + '\n처리 횟수: ' + count;
}
function handleClick() {
    count += 1;
    updateState();
}
document.querySelector('#register').addEventListener('click', function () {
    target.removeEventListener('click', handleClick);
    target.addEventListener('click', handleClick);
    active = true;
    updateState();
});
document.querySelector('#remove').addEventListener('click', function () {
    target.removeEventListener('click', handleClick);
    active = false;
    updateState();
});
updateState();

등록 후에는 테스트 클릭마다 횟수가 늘고, 제거 후에는 늘지 않습니다. 제거 자체는 기존 횟수를 지우지 않습니다. 아래 ‘초기화’는 핸들러를 제거하고 횟수도 0으로 돌립니다. 등록 전에 같은 핸들러를 제거하므로 등록 버튼을 반복해서 눌러도 중복 연결되지 않습니다.

removeEventListener()에는 등록할 때 사용한 같은 함수 객체가 필요합니다. 내용이 똑같아도 새로 만든 익명 함수는 다른 참조이므로 제거할 수 없습니다. 이벤트 종류와 capture 설정도 등록 때와 맞아야 합니다. 같은 요소에 같은 이벤트 종류·함수·capture로 addEventListener()를 반복 호출하면 중복 등록되지 않지만, jQuery .on()은 같은 핸들러도 여러 번 등록할 수 있습니다.

jQuery는 함수 참조 외에도 .off(".demo")처럼 네임스페이스로 제거 범위를 정할 수 있습니다. .off("click")는 해당 요소에 jQuery로 등록한 다른 click 처리까지 제거할 수 있으므로 범위를 좁히는 편이 좋습니다. .off()가 addEventListener()로 등록한 리스너를 제거하는 것은 아닙니다.

동적으로 생성된 요소의 이벤트 처리

#items에는 처음에 Test 1 버튼 하나가 있습니다. 비교를 위해 직접 등록과 이벤트 위임을 함께 연결합니다. ‘버튼 추가’ #add로 만든 Test 2 이후의 버튼은 직접 등록 시점에 없었던 요소입니다.

jQuery

let serial = 1;
let direct = 0;
let delegated = 0;
function updateState() {
    $('#result').text('직접 등록: ' + direct + '회\n이벤트 위임: ' + delegated + '회');
}
$('.item').on('click', function () {
    direct += 1;
    updateState();
});
$('#items').on('click', '.item', function () {
    delegated += 1;
    updateState();
});
$('#add').on('click', function () {
    serial += 1;
    const item = $('<button>', {
        type: 'button', class: 'item', text: 'Test ' + serial
    });
    $('#items').append(item);
    $('#result').text('Test ' + serial + ' 추가. 새 버튼을 눌러 비교하세요.');
});
updateState();

JavaScript

const items = document.querySelector('#items');
let serial = 1;
let direct = 0;
let delegated = 0;
function updateState() {
    document.querySelector('#result').textContent =
        '직접 등록: ' + direct + '회\n이벤트 위임: ' + delegated + '회';
}
const initialButtons = items.querySelectorAll('.item');
for (let i = 0; i < initialButtons.length; i++) {
    initialButtons[i].addEventListener('click', function () {
        direct += 1;
        updateState();
    });
}
items.addEventListener('click', function (event) {
    if (!(event.target instanceof Element)) {
        return;
    }
    const item = event.target.closest('.item');
    if (item === null || item === items || !items.contains(item)) {
        return;
    }
    delegated += 1;
    updateState();
});
document.querySelector('#add').addEventListener('click', function () {
    serial += 1;
    const item = document.createElement('button');
    item.type = 'button';
    item.className = 'item';
    item.textContent = 'Test ' + serial;
    items.append(item);
    document.querySelector('#result').textContent =
        'Test ' + serial + ' 추가. 새 버튼을 눌러 비교하세요.';
});
updateState();

Test 1을 누르면 직접 등록과 위임 횟수가 각각 1씩 늘어납니다. 새로 추가한 Test 2는 위임 횟수만 늘어납니다. 아래 초기화는 Test 1과 그 직접 핸들러를 남기고 추가한 버튼을 제거하며 두 횟수를 0으로 돌립니다.

직접 등록은 그 시점에 찾은 요소에만 적용됩니다. 새 요소마다 다시 연결할 수도 있지만, 유지되는 부모에서 click을 받으면 새 요소의 이벤트도 처리할 수 있습니다. 이것이 .on(event, selector, handler) 형태의 이벤트 위임입니다. JavaScript에서는 부모 리스너에서 target.closest()로 버튼을 찾고 부모 안의 요소인지 확인합니다.

jQuery 위임 핸들러의 this는 selector에 맞는 자식입니다. 위 JavaScript 코드의 currentTarget은 리스너를 등록한 #items이고, 찾은 item이 대상 버튼입니다. 부모 자체를 교체하면 리스너도 다시 연결해야 하며, 자식에서 버블링을 중단하면 부모의 위임 처리까지 도달하지 않습니다.

preventDefault()로 기본 동작 막기

‘페이지 내부 대상 링크’ #target의 href는 #destination입니다. #prevent checkbox를 체크하면 링크의 기본 이동과 해시 변경을 막습니다. 기본 동작 차단 여부와 현재 해시를 결과에 표시합니다.

jQuery

$('#target').on('click', function (event) {
    if ($('#prevent').prop('checked')) {
        event.preventDefault();
    }
    const prevented = event.isDefaultPrevented();
    window.setTimeout(function () {
        $('#result').text('defaultPrevented: ' + prevented
            + '\n현재 hash: ' + window.location.hash);
    }, 0);
});
$('#prevent').on('change', function () {
    history.replaceState(null, '', location.pathname + location.search);
    $('#result').text('해시를 초기화했습니다. 링크를 눌러 비교하세요.');
});

JavaScript

document.querySelector('#target').addEventListener('click', function (event) {
    if (document.querySelector('#prevent').checked) {
        event.preventDefault();
    }
    const prevented = event.defaultPrevented;
    window.setTimeout(function () {
        document.querySelector('#result').textContent =
            'defaultPrevented: ' + prevented + '\n현재 hash: ' + window.location.hash;
    }, 0);
});
document.querySelector('#prevent').addEventListener('change', function () {
    history.replaceState(null, '', location.pathname + location.search);
    document.querySelector('#result').textContent =
        '해시를 초기화했습니다. 링크를 눌러 비교하세요.';
});

체크하지 않으면 해시가 #destination으로 바뀌고, 체크하면 기본 이동을 취소합니다. setTimeout()은 취소하지 않은 링크의 기본 동작이 처리된 뒤 해시를 읽기 위해 사용합니다. 차단은 지연 함수가 아니라 클릭 핸들러 안에서 즉시 수행합니다. 예제가 iframe 안에 있으므로 결과의 해시는 바깥 블로그 페이지가 아닌 예제 문서의 값입니다.

preventDefault()는 링크 이동이나 form 제출 같은 기본 동작을 취소하며 이벤트가 부모로 전파되는 것을 막지는 않습니다. form이라면 submit 이벤트에서 적용할 수 있습니다. 취소 가능한 이벤트에만 효과가 있고 passive 리스너에서는 사용할 수 없습니다. jQuery는 isDefaultPrevented(), JavaScript는 defaultPrevented로 취소 여부를 확인합니다.

이벤트 버블링과 stopPropagation()

#parent 안에 ‘자식 클릭’ 버튼 #child가 있습니다. 자식 click 핸들러가 실행된 뒤 부모의 click 핸들러로 전달되는 순서를 줄 단위 로그로 확인합니다.

jQuery

const logs = [];
function addLog(text) {
    logs.push(text);
    $('#result').text(logs.join('\n'));
}
$('#child').on('click', function (event) {
    addLog('자식 click');
    if ($('#stop').prop('checked')) {
        event.stopPropagation();
        addLog('부모 전달 중지');
    }
});
$('#parent').on('click', function () {
    addLog('부모 click');
});
$('#clear').on('click', function () {
    logs.length = 0;
    $('#result').text('로그를 지웠습니다.');
});

JavaScript

const logs = [];
function addLog(text) {
    logs.push(text);
    document.querySelector('#result').textContent = logs.join('\n');
}
document.querySelector('#child').addEventListener('click', function (event) {
    addLog('자식 click');
    if (document.querySelector('#stop').checked) {
        event.stopPropagation();
        addLog('부모 전달 중지');
    }
});
document.querySelector('#parent').addEventListener('click', function () {
    addLog('부모 click');
});
document.querySelector('#clear').addEventListener('click', function () {
    logs.length = 0;
    document.querySelector('#result').textContent = '로그를 지웠습니다.';
});

체크하지 않고 자식 버튼을 누르면 ‘자식 click → 부모 click’ 순서로 쌓입니다. ‘stopPropagation 적용’을 체크하면 ‘자식 click → 부모 전달 중지’가 나오고 부모 로그는 추가되지 않습니다. 부모 영역 자체를 클릭하면 부모 로그만 추가됩니다. ‘로그 지우기’는 기록만 지우며 checkbox 설정은 유지합니다.

이 순서는 capture를 지정하지 않은 위 코드의 버블링 단계 기준입니다. 부모에 capture 리스너가 있다면 자식보다 먼저 실행될 수 있으며, 자식에서 전파를 막아도 이미 실행된 처리를 되돌리지 않습니다.

stopPropagation()은 전파를 막지만 기본 동작은 취소하지 않습니다. 링크 이동까지 막아야 한다면 preventDefault()도 별도로 호출합니다. 같은 요소의 다른 리스너 실행까지 중단하는 기능과도 구분해야 합니다. 또한 부모에 위임한 이벤트 처리도 막힐 수 있으므로 전파 중단은 필요한 곳에만 적용합니다.

jQuery와 JavaScript 주요 이벤트 문법 비교

기능 jQuery JavaScript
이벤트 등록 $target.on('click', handler) target.addEventListener('click', handler)
이벤트 제거 $target.off('click.demo', handler) target.removeEventListener('click', handler)
여러 요소에 등록 조회한 요소 집합에 .on() 각 요소에 addEventListener()
이벤트 위임 $parent.on('click', '.item', handler) 부모 리스너에서 target.closest()로 대상 확인
발생한 요소 event.target event.target
기본 동작 차단 event.preventDefault() event.preventDefault()
차단 여부 event.isDefaultPrevented() event.defaultPrevented
전파 중단 event.stopPropagation() event.stopPropagation()

입력 중 변화는 input, 선택이나 입력 확정은 change로 구분합니다. 제거할 핸들러의 참조를 보관하고, 동적으로 추가되는 요소에는 유지되는 부모로의 이벤트 위임을 고려하면 등록·제거 흐름을 관리하기 쉽습니다. 기본 동작 차단과 전파 중단은 목적이 다르므로 필요한 기능을 명시해서 사용합니다.

참고자료