JavaScript에서 이벤트 위임(Event Delegation) 기법이 무엇인지, 왜 사용해야 하는지, 그리고 어떻게 활용하는지 코드 예제와 함께 알아봅니다.
JavaScript 이벤트 위임이란 무엇이며, 언제 사용해야 할까?
간단히 말해, 이벤트 위임은 부모 요소에 단 하나의 이벤트 리스너만 등록하여 자식 요소들과 상호작용할 수 있게 해주는 JavaScript 기법입니다. 이벤트 위임을 사용하면 모든 자식 요소에 일일이 이벤트 핸들러를 추가할 필요 없이도 자식 요소를 찾아 조작할 수 있습니다.
이 기법이 얼마나 강력한지 곧 알게 될 것입니다.
예를 들어 리스트 안에 여러 개의 항목이 있다고 가정해 봅시다. 사용자가 항목을 클릭할 때마다 특정 동작이 실행되기를 원한다면, 이를 구현하는 방법은 크게 두 가지가 있습니다.
방법 1: 각 요소에 개별적으로 이벤트 리스너 연결하기
리스트의 각 항목에 접근하는 일반적인 방법은 for 루프 등으로 요소들을 순회한 후, 각 요소에 이벤트 리스너를 연결하는 것입니다.
let allExerciseItems = document.querySelectorAll(".exercise-item")
for (let exerciseItem of allExerciseItems) {
exerciseItem.addEventListener("click", function(event) {
// 대상 자식 요소로 원하는 작업 수행
event.target.classList.toggle("exercise-complete")
});
}
예시
방법 2: 이벤트 위임으로 항목에 접근하기
이벤트 위임은 앞서 본 for 루프 예제와는 완전히 다른 방식입니다. 접근하고 싶은 각 요소를 하나씩 순회하는 대신, 다음과 같이 부모 요소만 지정하면 됩니다.
// 부모 요소를 가져와 클릭 이벤트 리스너 추가
document.querySelector(".exercise-list").addEventListener("click", function(event) {
if(event.target.classList.contains('exercise-item')) {
// 대상 자식 요소로 원하는 작업 수행
event.target.classList.toggle('exercise-complete')
}
})
참고: 더 유연한 셀렉터 매칭이 필요하다면 classList.contains 대신 matches() API를 사용할 수 있습니다.
보시는 것처럼 결과는 완전히 동일합니다. 운동 목록의 어떤 항목을 클릭하든 exercise-complete 클래스가 토글됩니다. 달라진 것은 이 결과를 얻기 위해 사용한 방식입니다.
그렇다면 왜 이벤트 핸들링 + for 루프 방식 대신 이벤트 위임을 사용해야 할까요? 이어서 자세히 살펴보겠습니다!
이벤트 위임과 성능
하루, 한 시간, 심지어 1분마다 수백~수천 개의 동적 요소가 생성되는 웹 애플리케이션을 상상해 보세요. 호텔 예약 서비스의 예약 캘린더나, 최적의 사용자 경험을 위해 실시간 업데이트가 끊임없이 발생하는 X(트위터) 같은 소셜 미디어 플랫폼이 좋은 예입니다.
웹 UI의 모든 요소에 이벤트 리스너가 연결되어 있다면, 이런 시나리오에서는 하드웨어 부하가 순식간에 커질 수 있습니다.
수많은 이벤트 리스너는 사용자의 CPU를 과도하게 점유할 수 있으며, 메모리에는 방대한 함수 참조들이 쌓이게 됩니다. 이는 메모리 누수와 전반적인 성능 저하로 이어질 수 있습니다.
GC(가비지 컬렉션)에 대해 깊이 아는 개발자는 많지 않습니다. 필자 역시 주로 JavaScript라는 고수준 언어만 다뤄왔기 때문입니다. 그러나 분명한 사실 하나는, 브라우저의 GC는 Chrome, Firefox 등 벤더별로 다양한 브라우저와 스마트폰, 노트북 등 셀 수 없이 많은 디바이스 환경 때문에 예측하기 어렵다는 점입니다.
가비지 컬렉션(Garbage Collection)이란?
간단히 말해, 가비지 컬렉션은 컴퓨터 메모리 관리 방식의 하나입니다. 가비지 컬렉터는 메모리를 차지한 채 방치된 오래되거나 사용하지 않는 코드를 제거하여 시스템 메모리를 확보하려고 시도합니다. 프로그래밍 언어와 시스템에 따라 수많은 종류의 가비지 컬렉터가 존재합니다.
for 루프 예제에서 보았듯이 모든 요소에 이벤트 리스너를 연결하면 웹 앱에 필요한 가비지 컬렉션 작업량이 늘어나며, 이는 거의 필연적으로 성능 저하를 초래합니다.
따라서 일반적으로 모든 자식 요소에 각각 이벤트 리스너를 연결하는 것보다, 부모 요소에 단 하나의 이벤트 리스너를 두고 자식 요소들의 모든 클릭을 감지하는 것이 성능 면에서 훨씬 유리합니다.