기능 구현
- GNB Search Focus event , is-active
- GNB Search History is-active 일 때, background 클릭 시 is-active 비활성화
- GNB Search History 전체 삭제 기능
- GNB Search History 개별 삭제 기능
- 개별 삭제 시, 동시에 GNB Search가 닫히는 문제점 개선
const gnbSearch = document.querySelector('.gnb-search')
const gnbSearchInput = gnbSearch.querySelector('input')
const gnbSearchHistory = document.querySelector('.search-history')
function openSearchHistory() {
gnbSearchHistory.classList.add('is-active')
}
gnbSearchInput.addEventListener('focus', openSearchHistory)
input에 click를 하는 것이기 때문에, click event 보다는 focus event를 걸어주었다.
function closeGnbSearchHistory(e) {
if (!gnbSearch.contains(e.target)) {
gnbSearchHistory.classList.remove('is-active')
}
}
window.addEventListener('click', closeGnbSearchHistory)
contains
메서드를 사용하여 gnbSearch 를 포함하지 않는 공간을 클릭 했을 때, is-active가 비활성화되게 설정해주었다. 즉 gnbSearch가 is-active 상태일 때 (gnbSearch가 열렸을 때) background 영역을 클릭 시 is-active가 비활성화 시키며, gnbSearch를 닫히게 하는 기능이다.
전체 삭제 버튼을 클릭하면, ol 안의 모든 li태그들이 삭제가 되어져야 한다.
const gnbSearchHistory = document.querySelector('.search-history')
const gnbSearchHistoryList = gnbSearchHistory.querySelector('ol')
const deleteAllButton = gnbSearchHistory.querySelector(
'.search-history-header button'
)
function deleteAllSearchHitstory(e) {
const searchItem = gnbSearchHistoryList.children
// HTMLCollection은 유사배열객체이기 때문에 foreach를 사용할 수 없다.
// => 따라서 Array.from 을 이용하여 배열을 생성해준 뒤, forEach를 사용한다
Array.from(searchItem).forEach((item) => item.remove())
}
deleteAllButton.addEventListener(
'click',
deleteAllSearchHitstory
)
리팩토링 전의 방법은, searchItem
에 ol의 자식 태그인 li 태그들을 모두 담아서, 고차 함수인 forEach 메서드를 사용하여 item 당 remove 메서드를 걸어주었다.
모든 li태그에게 remove 메서드를 걸어준다는 것은 비효율적인 것 같아 다른 방법으로 간결하게 구현하였다.
deleteAllButton.addEventListener('click', function () {
gnbSearchHistoryList.innerHTML = ''
gnbSearchHistory.classList.remove('is-active')
})
리팩토링을 한 결과, 짧은 코드만으로 전체 삭제
기능이 완성되었다.
ol태그 의 innerHTML 을 빈 값으로 처리해준다면 li태그들을 모두 삭제하는 결과를 한 번에 구현할 수 있게 된다.
function deleteSearchHistoryItem(e) {
e.target.closest('li').remove()
}
deleteButton.addEventListener('click', deleteSearchHistoryItem)
첫 번째 방법으로는, 삭제하고 싶은 li태그의 삭제 버튼
을 클릭하여, e.target.closest('li')를 찾으면 해당 검색어의 li태그를 찾을 수 있다. 그것을 remove 해주어 개별적으로 삭제하는 기능을 구현하였다.
function deleteSearchHistoryItem() {
const itemToDelete = this.parentNode
gnbSearchHistoryList.removeChild(itemToDelete)
}
deleteButton.addEventListener('click', deleteSearchHistoryItem)
두 번째 방법으로는, 클릭한 삭제 버튼의 parentNode를 itemToDelete
변수에 지정해주었다. 즉 itemToDelete이 li태그가 되는 것이다. li의 부모 태그인 ol태그인 gnbSearchHistoryList
와 자식 태그인 li 태그 gnbSearchHistoryList
두 태그를 removeChild
메서드를 사용하여 개별적으로 삭제하는 기능을 구현했다.
remove 와 removeChild의 차이점?
=> 프로젝트를 진행하다가, remove 와 removeChild의 차이점이 문득 궁금해져서 정리를 해보았다.
위의 GNB Search History 개별 삭제 기능을 구현하다가, 개별적으로 삭제 시에 동시에 GNB Search 가 닫히는 버그가 발생한다. 이것은 개발을 하는 측면에서 예상하지 못했던 것이기도 하였고 UX측면에서도 좋지 않다.
우선 동시에 닫힌다는 것은 classList.remove() 이벤트가 예상치 못한 곳에서 실행되었다는 점이다.
function closeGnbSearchHistory() {
gnbSearchHistory.classList.remove('is-active')
window.removeEventListener('click', closeGnbSearchHistoryOnClickingOutside)
}
// gnbSearch is-active일 때 background 클릭 시, gnbSearch 닫히게 하는 함수
function closeGnbSearchHistoryOnClickingOutside(e) {
if (!gnbSearch.contains(e.target)) {
console.log("Close!");
closeGnbSearchHistory()
}
}
// gnbSearch 개별 item 삭제
function deleteSearchHistoryItem(e) {
console.log("Delete!");
const itemToDelete = this.parentNode
gnbSearchHistoryList.removeChild(itemToDelete)
}
deleteButtonList.forEach((button) => {
button.addEventListener('click', deleteSearchHistoryItem)
})
closeGnbSearchHistoryOnClickingOutside() 함수에서 classList.remove 를 사용하는 것을 볼 수 있다.
deleteSearchHistoryItem, closeGnbSearchHistoryOnClickingOutside 함수에서 각각 console.log를 출력해보았더니, 개별적인 Item을 삭제 버튼을 클릭
할 때 Delete가 찍힌 후, Close가 찍힌 것을 확인해볼 수 있다.
부모/자식 구조에서 동일한 click 이벤트가 등록이 되어져 있어, 버블링이 발생하는 것이었다.
자식 함수에 click을 누르면 부모 함수의 click 이벤트도 같이 실행되어지기 때문에 자식인 Delete가 먼저 실행되고 난 후, 부모인 Close가 실행되는 것이었다.
이때 자식 event만 실행시키고 싶을 때에는 2가지 방법이 존재한다.
- stopPropagation
- stopPropagation() 메서드는 현재 이벤트가 캡처링/버블링 단계에서 더 이상 전파되지 않도록 방지한다.
- target과 currentTarget을 비교
- 부모에서 이벤트에 있는 target과 currentTarget이 똑같지 않으면 return을 해준다.
( 부모는 target과 currentTarget이 다르기 때문에 더 이상 처리되지 않는다. )
function deleteSearchHistoryItem(e) {
e.stopPropagation() // 버블링 전파 방지
console.log('Delete!')
const itemToDelete = this.parentNode
gnbSearchHistoryList.removeChild(itemToDelete)
if (gnbSearchHistoryList.children.length === 0) {
closeGnbSearchHistory()
}
}
위와 같이 stopPropagation() 메서드를 자식의 이벤트에 적용시켰을 때, 부모에게 전파되는 버블링 단계를 전파되지 않도록 방지하는 방법이다. 그러나 stopPropagation 메서드를 사용하는 것은 좋지 않은 방법이다. 왜냐하면 다른 이벤트가 어떤 특별한 기능을 수행할 수 있는데, 본인의 이벤트만 처리하고 다른 이벤트를 수행하는 것들을 다 취소해버리는 방법이기 때문에 위험한 방법이다. 혹여나 프로젝트 규모가 커지면 이것 때문에 예상하지 못한 오류가 발생되어서 디버깅을 오랫동안 할 수 있을 것이다. 그러므로 웬만하면 쓰지 않는 것이 좋다.
function closeGnbSearchHistoryOnClickingOutside(e) {
if (e.target !== e.currentTarget) return
if (!gnbSearch.contains(e.target)) {
console.log('Close!')
closeGnbSearchHistory()
}
}
부모에서 이벤트에 있는 target과 currentTarget이 똑같지 않으면 return을 해준다.
( 부모는 target과 currentTarget이 다르기 때문에 더 이상 처리되지 않는다. )
버블링 문제점을 개선하고 나니, 개별적으로 gnbSearch Item 을 삭제하여도 동시에 gnbSearch가 닫히지 않는 문제가 해결되었다.