목적
.AccordionItem
에 .AccordionItem--open
상태 클래스를 추가/제거해서 .AccordionHead
를 클릭했을 때 .AccordionBody
가 open/close되는 인터랙션을 구현한다.
Copy <div class="AccordionItem">
<!-- AccordionItem--open 본문 펼침 상태 클래스 -->
<dt class="AccordionHead">넷플릭스란 무엇인가요? <button type="button" class="resetButton AccordionButton"><img src="./assets/plusIcon.svg" alt="펼침" width="24" height="24" /></button></dt>
<dd class="AccordionBody">
<p>넷플릭스는 각종 수상 경력에 빛나는 TV 프로그램, 영화, 애니메이션, 다큐멘터리 등 다양한 콘텐츠를 인터넷 연결이 가능한 수천 종의 디바이스에서 시청할 수 있는 스트리밍
서비스입니다.</p>
<p>저렴한 월 요금으로 일체의 광고 없이 원하는 시간에 원하는 만큼 즐길 수 있습니다. 무궁무진한 콘텐츠가 준비되어 있으며 매주 새로운 TV 프로그램과 영화가 제공됩니다.</p>
</dd>
</div>
설계
변수 설정
Copy const ACCORDION_ITEM_ACTIVE_CLASS = 'AccordionItem--open'
const accordionNode = document .querySelector ( '.Accordion' )
const accordionItemNodeList = accordionNode .querySelectorAll ( '.AccordionItem' )
사용된 기능
방법
.AccordionItem
에 'click'
이벤트를 적용한다.
'click'
했을 때, classList.contains()
메서드를 사용해서 .AccordionItem--open
가 존재하는지 확인한다.
클릭 할 때마다 open/close를 반복해야 하기 때문에 open 상태 클래스가 존재 하지 않는다면 classList.add()
를 사용해서 추가, 존재한다면 classList.remove()
메서드를 사용해서 제거한다.
처음에 작성한 코드
Copy const handleToggleAccordionBody = function (e) {
const accordionItemNode = accordionNode .querySelector ( '.AccordionItem' )
if ( ! accordionItemNode . classList .contains ( ACCORDION_ITEM_ACTIVE_CLASS )) {
accordionItemNode . classList .add ( ACCORDION_ITEM_ACTIVE_CLASS )
} else {
accordionItemNode . classList .remove ( ACCORDION_ITEM_ACTIVE_CLASS )
}
}
for ( var i = 0 ; i < accordionItemNodeList . length ; i ++ ) {
accordionItemNodeList[i] .addEventListener ( 'click' , handleToggleAccordionBody)
}
문제점 발견
Copy const handleToggleAccordionBody = function (e) {
const accordionItemNode = accordionNode .querySelector ( '.AccordionItem' )
}
문제 : 어떤 Item을 클릭해도 첫 번째 Item만 동작이 되었다.
원인 : 첫 번째 .AccordionItem
만 찾아왔기 때문이다.
문제 : e.target
을 사용해서 이벤트가 걸린 요소를 확인 하지 않았다.
그러다 보니 계속 밖에서만 .AccordionItem
요소에 접근하려고 하고 안에서 접근할 생각을 하지 못했다.
원인 : .AccordionItem
에 이벤트를 걸었기 때문에 e.target
이 당연히 .AccordionItem
라고 생각했다. handleToggleAccordionBody
의 e.target
은 .AccordionButton
이다.
개선 (1)
Node.parentNode 사용하기
.AccordionButton
은 .AccordionItem
의 자식요소입니다. 자식요소에서 부모요소에 접근하는 Node.parentNode
을 사용하여 클릭한 요소의 부모요소에 접근합니다.
Copy const handleToggleAccordionBody = function (e) {
const accordionItemNode = e . target . parentNode .parentNode
}
Node.parentNod 는 인접한 부모요소를 찾기 때문에 부모의 부모를 찾으려면 .parentNode.parentNode
으로 작성한다.
개선 (2)
Element.closest() 사용하기
Node.parentNode
보다 더 개선된 메서드 사용합니다. Element.closest()
은 선택한 요소에서 인접한 부모요소부터 클래스 이름이 일치하는 부모요소를 찾는다.
Copy const handleToggleAccordionBody = function (e) {
const accordionItemNode = e . target .closest ( '.AccordionItem' )
}
개선 (3)
forEach 사용하기
for문
대신 유사배열인 .AccordionItem
을 배열화하여 forEach()
메서드 를 사용합니다.
Copy const accordionItemNodeList = accordionNode .querySelectorAll ( '.AccordionItem' )
const accordionArr = Array .from (accordionItemNodeList)
accordionArr .forEach ( function (item) {
item .addEventListener ( 'click' , handleToggleAccordionBody)
})
개선 (4)
add()/ remove()
메서드 대신 classList.toggle() 메서드 를 사용합니다.
classList.toggle()
는 인자에 전달된 클래스 이름의 유무를 확인하고 만약 있다면 선택된 요소에 제거하고 없다면 제거하는 기능을 하는 메서드입니다.
즉, toggle()
메서드는 contains()
, add()
, remove()
메서드의 기능을 한 번에 수행합니다.
Copy const handleToggleAccordionBody = function (e) {
const accordionItemNode = e . target .closest ( '.AccordionItem' )
accordionItemNode . classList .toggle ( ACCORDION_ITEM_ACTIVE_CLASS )
}
개선 (5)
Element.closest()에서 null이 반환되는 경우 오류를 방지하기
Element.closest()
closest()
메서드는 element node
에 사용이 가능한 메서드입니다. 메서드에 전달된 클래스 이름과 일치하는 가장 가까운 부모가 있을 때 부모 노드를 반환하는 메서드 입니다. 단, 일치하는 부모노드가 없을 경우 null
을 반환합니다.
null
을 반환하기 때문에 조건문에서 사용이 가능합니다.
null = false, 일치하는 노드가 있을 경우 = true 를 반환합니다.
논리 연산자를 사용하기
Copy const handleToggleAccordionBody = function (e) {
const accordionItemNode = e . target .closest ( '.AccordionIte' )
/* 방법 1 */
/* accordionItemNode이 null이라면 */
accordionItemNode &&
accordionItemNode . classList .toggle ( ACCORDION_ITEM_ACTIVE_CLASS )
/* 방법 2 */
/* accordionItemNode이 null이 아니라면 */
accordionItem !== null &&
accordionItem . classList .toggle ( ACCORDION_ITEM_ACTIVE_CLASS )
}
조건문 사용하기
Copy const handleToggleAccordionBody = function (e) {
const accordionItemNode = e . target .closest ( '.AccordionItem' )
/* 방법 1 */
/* accordionItemNode이 null이라면 */
if (accordionItemNode) {
accordionItemNode . classList .toggle ( ACCORDION_ITEM_ACTIVE_CLASS )
}
/* 방법 2 */
/* accordionItemNode이 null이 아니라면 */
if (accordionItem !== null ) {
accordionItem . classList .toggle ( ACCORDION_ITEM_ACTIVE_CLASS )
} else {
/* 디버깅을 위해 자세한 오류 메세지를 입력해주는 것을 권 */
console .error (
'클릭한 버튼의 부모노드 중 일치하는 대상이 없습니다. closest 메서드에 전달된 인자를 확인하세요.'
)
}
}
결론
Copy const ACCORDION_ITEM_ACTIVE_CLASS = 'AccordionItem--open'
const accordionNode = document .querySelector ( '.Accordion' )
const accordionItemNodeList = accordionNode .querySelectorAll ( '.AccordionItem' )
const accordionArr = Array .from (accordionItemNodeList)
const handleToggleAccordionBody = function (e) {
const accordionItemNode = e . target .closest ( '.AccordionItem' )
if (accordionItemNode !== null ) {
accordionItemNode . classList .toggle ( ACCORDION_ITEM_ACTIVE_CLASS )
} else {
console .error (
'클릭한 버튼의 부모노드 중 일치하는 대상이 없습니다. closest 메서드에 전달된 인자를 확인하세요.'
)
}
}
accordionArr .forEach ( function (item) {
item .addEventListener ( 'click' , handleToggleAccordionBody)
})
주의
메서드를 사용하기 전에 꼭 브라우저 호환성을 확인하기
addEventListener
를 사용할 때는 e.target
을 사용해서 이벤트가 걸려있는 요소 확인하기
코드를 작성하고 나서 리팩토링하는 작업을 꼭 한다.
불필요한 코드(예, 주석, console.log()
등)을 정리하고 개선할 사항이 있는지 확인한다.