오늘의집 Clone Coding - GNB Search History Input 검색

HYl·2022년 6월 14일
0

오늘의집 Project

목록 보기
3/3

기능 구현

  • GNB Search Focus event , is-active
  • GNB Search History is-active 일 때, background 클릭 시 is-active 비활성화
  • GNB Search History 전체 삭제 기능
  • GNB Search History 개별 삭제 기능
    • 개별 삭제 시, 동시에 GNB Search가 닫히는 문제점 개선

GNB Search Focus 이벤트

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를 닫히게 하는 기능이다.


GNB Search History 전체 삭제 기능

전체 삭제 버튼을 클릭하면, 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태그들을 모두 삭제하는 결과를 한 번에 구현할 수 있게 된다.


GNB Search History 개별 삭제 기능

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가 닫히는 문제점


위의 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이 다르기 때문에 더 이상 처리되지 않는다. )

1. stopPropagation을 사용하여 버블링 문제 해결

function deleteSearchHistoryItem(e) {
  e.stopPropagation() // 버블링 전파 방지
  console.log('Delete!')
  const itemToDelete = this.parentNode
  gnbSearchHistoryList.removeChild(itemToDelete)

  if (gnbSearchHistoryList.children.length === 0) {
    closeGnbSearchHistory()
  }
}

위와 같이 stopPropagation() 메서드를 자식의 이벤트에 적용시켰을 때, 부모에게 전파되는 버블링 단계를 전파되지 않도록 방지하는 방법이다. 그러나 stopPropagation 메서드를 사용하는 것은 좋지 않은 방법이다. 왜냐하면 다른 이벤트가 어떤 특별한 기능을 수행할 수 있는데, 본인의 이벤트만 처리하고 다른 이벤트를 수행하는 것들을 다 취소해버리는 방법이기 때문에 위험한 방법이다. 혹여나 프로젝트 규모가 커지면 이것 때문에 예상하지 못한 오류가 발생되어서 디버깅을 오랫동안 할 수 있을 것이다. 그러므로 웬만하면 쓰지 않는 것이 좋다.

2. target과 currentTarget을 비교하여 버블링 문제 해결

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가 닫히지 않는 문제가 해결되었다.

profile
꾸준히 새로운 것을 알아가는 것을 좋아합니다.

0개의 댓글