웹 개발에서 버튼 클릭이나 폼 제출 같은 사용자 상호작용을 처리할 때, 많은 개발자들이 HTML onclick 속성과 JavaScript addEventListener() 메서드 중 뭘 써야 할지 헷갈려 한다. 둘 다 이벤트를 감지하고 처리하는 기능을 하지만, 실제로는 완전히 다른 방식으로 동작한다. 이번에는 두 방법의 정확한 차이가 뭔지, 언제 뭘 써야 하는지, 그리고 어떻게 성능 최적화까지 달성할 수 있는지 완벽하게 정리해서 소개하겠다.
onclick은 HTML 태그에 직접 인라인으로 쓰는 가장 오래된 방식이다. 브라우저가 HTML을 파싱할 때 onclick 속성을 만나면 거기에 바인딩된 함수를 즉시 등록한다. 반면 addEventListener()는 DOM 객체에 이벤트 리스너를 추가하는 방식이다. 같은 이벤트에 여러 리스너를 등록할 수 있고, 나중에 언제든 removeEventListener()로 제거할 수도 있다.
실무에서 대부분의 개발자들은 onclick이 간단하니까 별 생각 없이 쓰거나, 반대로 최신 방식이라고 해서 addEventListener()를 무조건 써야 한다고 생각한다. 하지만 코드 유지보수성, 메모리 관리, 이벤트 우선순위 같은 측면에서는 훨씬 더 복잡한 문제들이 숨어 있다.
HTML에 직접 JavaScript 코드를 쓰는 방식이다. 문제는 여러 가지다. 첫째, 같은 이벤트에 여러 핸들러를 등록할 수 없다. 두 번째 핸들러가 첫 번째 것을 덮어버린다. 둘째, HTML과 JavaScript가 강하게 결합되어 나중에 코드를 수정할 때 HTML 파일까지 건드려야 한다. 셋째, 전역 스코프에 함수를 노출해야 하는 경우가 많아 변수명 충돌 위험이 높다.
<button onclick="handleClick()">클릭</button>
<button onclick="handleClick()">클릭2</button>
<script>
function handleClick() {
console.log('버튼 클릭됨');
}
</script>
위 코드를 실행하면 각 버튼마다 handleClick() 함수가 호출된다. 하지만 같은 버튼에 대해 두 번째 onclick을 추가하려면? 불가능하다. HTML을 수정해야 한다. 그리고 handleClick 함수명이 전역에 떠서 다른 라이브러리와 이름이 충돌할 수 있다.
JavaScript 파일(또는 script 태그)에서 DOM 요소를 선택한 후 이벤트 리스너를 등록한다. 같은 요소에 여러 리스너를 등록할 수 있고, 각 리스너를 개별적으로 제거할 수도 있다. 함수는 전역 스코프가 아닌 모듈 내부에 정의할 수 있어 네임스페이스 충돌을 피할 수 있다.
<button class="btn">클릭</button>
<button class="btn">클릭2</button>
<script>
const buttons = document.querySelectorAll('.btn');
function handleClick() {
console.log('버튼 클릭됨');
}
function handleAnalytics() {
console.log('분석 데이터 전송');
}
buttons.forEach(btn => {
btn.addEventListener('click', handleClick);
btn.addEventListener('click', handleAnalytics);
});
</script>
같은 버튼에 handleClick과 handleAnalytics 두 개의 리스너가 등록되어, 클릭할 때마다 둘 다 순서대로 실행된다. 나중에 handleAnalytics를 제거하고 싶으면 removeEventListener('click', handleAnalytics)만 호출하면 된다. HTML은 전혀 건드릴 필요가 없다.
| 항목 | onclick 속성 | addEventListener() |
|---|---|---|
| 같은 이벤트 여러 핸들러 | 불가능 (덮어쓰기) | 가능 |
| HTML과 JS 분리 | 불가능 | 가능 |
| 리스너 제거 | 어려움 (null 할당) | removeEventListener() |
| 이벤트 옵션 (capture 등) | 제한적 | 완전 지원 |
| 성능 | 약간 빠름 | 약간 느림 (무시할 수준) |
| 메모리 관리 | 암시적 | 명시적 제거 필요 |
JavaScript로 새로운 DOM 요소를 동적으로 생성하는 경우를 생각해보자. onclick으로 처리하면 처음부터 HTML에 있던 요소들은 괜찮지만, 나중에 추가된 요소들은 onclick 속성이 없어서 이벤트가 작동하지 않는다. addEventListener()를 쓰더라도 동적으로 생성된 요소마다 일일이 리스너를 등록해야 하는 번거로움이 생긴다.
이 문제는 이벤트 위임(Event Delegation)으로 해결한다. 부모 요소에 리스너를 등록하고, 자식 요소의 클릭 이벤트를 감지한 후 event.target으로 실제 클릭한 요소를 확인한다.
// ✗ 잘못된 방식: 모든 동적 요소마다 리스너 등록
function addNewItem() {
const li = document.createElement('li');
li.textContent = '새 항목';
li.addEventListener('click', handleItemClick);
document.getElementById('list').appendChild(li);
}
// ✓ 올바른 방식: 부모에 이벤트 위임
document.getElementById('list').addEventListener('click', (e) => {
if (e.target.tagName === 'LI') {
console.log(e.target.textContent + ' 클릭됨');
}
});
function addNewItem() {
const li = document.createElement('li');
li.textContent = '새 항목';
document.getElementById('list').appendChild(li);
}
두 번째 방식이 훨씬 효율적이다. 리스너는 한 번만 등록하고, 아무리 많은 항목을 동적으로 추가해도 새로 리스너를 등록할 필요가 없다.
addEventListener()는 세 번째 인자로 옵션 객체를 받을 수 있다. capture, once, passive 같은 옵션들이다. onclick으로는 이런 제어가 거의 불가능하다.
// once: 리스너가 한 번만 실행되고 자동 제거
button.addEventListener('click', handleClick, { once: true });
// capture: 버블링 단계 대신 캡처 단계에서 이벤트 감지
button.addEventListener('click', handleClick, { capture: true });
// passive: 스크롤 성능 최적화 (preventDefault() 호출 안 함)
document.addEventListener('touchmove', handleScroll, { passive: true });
특히 passive 옵션은 모바일에서 스크롤 성능을 크게 향상시킨다. 브라우저가 이벤트 핸들러가 preventDefault()를 호출할 리 없다는 걸 미리 알면, 스크롤을 즉시 시작할 수 있기 때문이다.
addEventListener()로 등록한 리스너는 요소가 DOM에서 제거되어도 참조가 남아있으면 메모리를 계속 차지한다. 큰 프로젝트나 SPA(Single Page Application)에서 페이지를 전환할 때마다 요소를 제거한다면, 리스너도 반드시 제거해야 한다.
// 리스너를 변수에 저장해서 나중에 제거할 수 있게
const handleClick = (e) => {
console.log('클릭됨');
};
button.addEventListener('click', handleClick);
// 페이지 전환 또는 모달 닫기 시
button.removeEventListener('click', handleClick);
리스너를 화살표 함수로 직접 등록하면 나중에 제거할 수 없다. 반드시 이름 있는 함수로 정의하거나, 람다 함수를 변수에 저장해야 한다.
addEventListener()는 onclick보다 훨씬 강력하고 유연하다. 같은 이벤트에 여러 리스너를 등록할 수 있고, 리스너를 제거할 수 있으며, 이벤트 옵션으로 성능까지 최적화할 수 있다. HTML과 JavaScript를 깔끔하게 분리할 수 있어 유지보수도 쉽다. 작은 최적화와 올바른 관행이 모여서 안정적이고 확장 가능한 코드를 만든다는 점을 기억하자. addEventListener()를 기본으로, 꼭 필요한 경우에만 onclick을 쓰는 습관을 들이면, 나중에 버그를 추적하거나 기능을 추가할 때 훨씬 수월할 것이다.