2024년 3월 17일
이번 점심 뭐먹지 미션에서 음식점 목록에서 각각 항목의 즐겨찾기 버튼을 클릭했을시에, 아이콘이 변경되어야하는 기능 구현이 필요했다.
export const clickFavoriteIconHandler = () => {
document.addEventListener('DOMContentLoaded', () => {
const restaurantItems = document.querySelectorAll('.restaurant');
restaurantItems.forEach((item) => {
item.addEventListener('click', function (this: HTMLElement) {
const restaurantId = this.dataset.id;
console.log('restaurantId : ', restaurantId);
changeFavoriteIconState(restaurantId ?? '');
});
});
});
};
function changeFavoriteIconState(restaurantId: string) {
// id 값을 받아서 UI 변경하는 로직
}
초기에 이 기능을 구현하기 위해서 ul
태그를 선택해서 반복문을 돌며 각각의 li
태그에 이벤트를 부착해주는 식으로 기능을 구현했었다.
하지만 여기서 문제점은, 하나의 li
태그를 클릭하더라도 콘솔 값이 모든 li
태그의 갯수만큼 발생했다.
restaurantId : 1
restaurantId : 1
...
1번 요소를 클릭했음에도 해당 요소 갯수만큼 콘솔값이 나왔다.
이 문제의 원인은 이벤트 핸들러가 중복으로 등록 됐기 때문인데, 예를 들어서 사용자가 해당 요소를 클릭했을 당시에, 이벤트 핸들러가 여러번 등록이 되는 것이다.
그리고 단일 음식점 아이템들 같은 경우 동적으로 음식점이 추가가 될수 있는 부분이다.
하지만 이런 경우에 clickFavoriteIconHandler
가 새로운 요소들에 대해서 다시 호출이 되면서 이미 이벤트 리스너가 등록된 요소들에 중복으로 이벤트 리스너가 추가될 수도 있다.
그래서 이러한 문제를 해결하기 위해서 해결 방법을 찾던 중 이벤트 위임
방식을 알게 되었다.
이벤트 위임
은 하나의 상위 요소가 있을때, 해당 요소의 자식 요소에서 발생하는 이벤트를 처리하기 위해서 상위 요소에서 이벤트 리스너를 등록하는 방식이다.
<body id="app">
<ul id="parent">
<li class="child">아이템 1</li>
<li class="child">아이템 2</li>
<li class="child">아이템 3</li>
</ul>
</body>
<script type="module">
const parent = document.getElementById('parent');
parent.addEventListener('click', (e) => {
if (e.target && e.target.nodeName === 'LI') {
console.log(e.target.textContent + '을 클릭');
}
});
</script>
여기서 선택자로 parent
를 가져왔지만, 상위 요소에 이벤트를 바인딩 해줌으로써 하위 요소의 값들을 가져오는 것을 확인할 수 있다.
각 요소마다 이벤트리스너를 바인딩 해줄 필요 없고 상위에서 한번만 등록해주면 된다.
또한 동적으로 아이템들이 추가되더라도 그에 따른 이벤트 바인딩도 자동으로 적용 되므로 성능상 이점 또한 누릴 수 있다.
이 이벤트 위임의 개념을 바탕으로 음식점 목록에서의 즐겨찾기 버튼 또한 동적으로 변경해줄 수 있었다.
export const bindChangeFavoriteIconStateHandler = () => {
const listContainer = document.querySelector('.restaurant-list');
if (isHTMLElement(listContainer)) {
listContainer.addEventListener('click', favoriteIconEventPhaseHandler);
}
};
여기서 먼저 restaurant-list
의 클래스를 가진 ul
태그를 선택해주고 클릭시에 favoriteIconEventPhaseHandler
함수가 발생하도록 바인딩 해준다.
const favoriteIconEventPhaseHandler = (event: Event) => {
const target = event.target as Element;
console.log('favoriteIconEventPhaseHandler 내에서의 event.target : ', target);
const favoritedIcon = target.closest('.favorited-icon');
if (!isHTMLElement(favoritedIcon)) return;
const restaurantId = getRestaurantIdFromListItem(favoritedIcon);
if (restaurantId !== undefined) {
changeFavoriteState(restaurantId);
}
};
콘솔창으로 target
의 값을 확인해보면 해당 li 태그 안에 가장 큰 div
컨테이너가 나온다.
여기서 closest
메서드를 통해서, 해당 컨테이너 내부에 이미지 태그를 타겟 요소로 선택해준다.
여기서 closest
이라는 메서드는, 해당 target 요소에서 선택자 값을 가진 가장 가까운 조상 요소를 찾아준다.
저 favoritedIcon
같은 경우엔 선택한 이미지 태그가 나오고, 아이콘을 클릭하지 않고 그냥 li
태그 자체를 클릭했다면 null
값이 나온다.
const getRestaurantIdFromListItem = (element: Element) => {
const listItem = element.closest('li');
if (isHTMLElement(listItem)) {
const restaurantId = Number(listItem.dataset.id);
return !Number.isNaN(restaurantId) ? restaurantId : undefined;
}
return undefined;
};
이 함수는 이미지 태그를 클릭했을때, 그 상위 요소인 li
태그의 dataset.id 값을 가져오는 함수이다.
const changeFavoriteState = (restaurantId: number) => {
RestaurantListStorageService.patchData(restaurantId);
const allRestaurants = RestaurantListStorageService.getData();
const targetRestaurant = allRestaurants?.find((restaurant) => restaurant.id === restaurantId);
if (targetRestaurant) {
const listComponent = generateRestaurantListItemComponent(targetRestaurant);
updateRestaurantListItemUI(restaurantId, listComponent);
}
};
이 함수는 실질적으로 즐겨찾기 버튼에 대한 컴포넌트를 변경 시켜주는 함수이다.
getRestaurantIdFromListItem
에서 얻은 li
태그의 아이디 값을 받아와서 실질적인 데이터 업데이트를 해주고 UI를 업데이트 해준다.
하나의 태그 안에서 이벤트가 여러개 발생하거나 중복되는 하위 요소들이 많은 경우들이 많다.
이런 경우에 이벤트 위임과 closest 메서드를 사용한다면 필요한 요소의 이벤트만 바인딩 해줄수 있어서 유용한것 같다고 생각한다.