> For the complete documentation index, see [llms.txt](https://shhn0509.gitbook.io/make-better-netflix-website/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://shhn0509.gitbook.io/make-better-netflix-website/interaction/javascript/accordion-control.md).

# Accordion Control

## 목적  <a href="#purpose" id="purpose"></a>

`.AccordionItem`에 `.AccordionItem--open` 상태 클래스를 추가/제거해서 `.AccordionHead`를 클릭했을 때 `.AccordionBody`가 open/close되는 인터랙션을 구현한다.&#x20;

```markup
<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>
```

{% embed url="<https://youtu.be/Oj1cpB6ucUs>" %}
Accordion Control 예시
{% endembed %}

## 설계   <a href="#plan" id="plan"></a>

### 변수 설정

```typescript
const ACCORDION_ITEM_ACTIVE_CLASS = 'AccordionItem--open'
const accordionNode = document.querySelector('.Accordion')
const accordionItemNodeList = accordionNode.querySelectorAll('.AccordionItem')
```

### 사용된 기능 <a href="#using-skill" id="using-skill"></a>

* [Array.from()](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/from)
* [Element.closest()](https://developer.mozilla.org/en-US/docs/Web/API/Element/closest)
* [classList.toggle()](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList#Examples)
* [Array.prototype.forEach()](https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach)

### 방법  <a href="#way-1" id="way-1"></a>

* `.AccordionItem`에 `'click'` 이벤트를 적용한다.&#x20;
* `'click'` 했을 때,  `classList.contains()` 메서드를 사용해서 `.AccordionItem--open`가 존재하는지 확인한다.&#x20;
* 클릭 할 때마다 open/close를 반복해야 하기 때문에 open 상태 클래스가 존재 하지 않는다면 `classList.add()`를 사용해서 추가, 존재한다면 `classList.remove()` 메서드를 사용해서 제거한다.

### 처음에 작성한 코드&#x20;

```typescript
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)
}
```

### 문제점 발견 &#x20;

```typescript
const handleToggleAccordionBody = function (e) {
  const accordionItemNode = accordionNode.querySelector('.AccordionItem')
}

```

* **문제 :** 어떤 Item을 클릭해도 첫 번째 Item만 동작이 되었다.&#x20;
  * **원인** : 첫 번째 `.AccordionItem`만 찾아왔기 때문이다.&#x20;
* **문제** : `e.target`을 사용해서 이벤트가 걸린 요소를 확인 하지 않았다.\
  그러다 보니 계속 밖에서만 `.AccordionItem` 요소에 접근하려고 하고 안에서 접근할 생각을 하지 못했다.&#x20;
  * **원인** : `.AccordionItem`에 이벤트를 걸었기 때문에 `e.target`이 당연히 `.AccordionItem`라고 생각했다. `handleToggleAccordionBody`의 `e.target`은 `.AccordionButton`이다.&#x20;

![handleToggleAccordionBody의 e.target](/files/-MPi_EU3oBhQoOm77_af)

### 개선 (1) <a href="#improvement-1" id="improvement-1"></a>

#### Node.parentNode 사용하기&#x20;

`.AccordionButton`은 `.AccordionItem`의 자식요소입니다. 자식요소에서 부모요소에 접근하는 [`Node.parentNode`](https://developer.mozilla.org/en-US/docs/Web/API/Node/parentNode)을 사용하여 클릭한 요소의 부모요소에 접근합니다.&#x20;

```typescript
const handleToggleAccordionBody = function (e) {
  const accordionItemNode = e.target.parentNode.parentNode
}
```

![handleToggleAccordionBody의 e.target.parentNode.parentNode](/files/-MPicNMV6V6buEbLaqR4)

{% hint style="info" %}
[Node.parentNod](https://developer.mozilla.org/en-US/docs/Web/API/Node/parentNode)는 인접한 부모요소를 찾기 때문에 부모의 부모를 찾으려면  `.parentNode.parentNode`으로 작성한다.&#x20;
{% endhint %}

### 개선 (2)  <a href="#improvement-2" id="improvement-2"></a>

#### Element.closest() 사용하기&#x20;

`Node.parentNode` 보다 더 개선된 메서드 사용합니다. `Element.closest()`은 선택한 요소에서 인접한 부모요소부터 클래스 이름이 일치하는 부모요소를 찾는다.&#x20;

```typescript
const handleToggleAccordionBody = function (e) {
  const accordionItemNode = e.target.closest('.AccordionItem')
}
```

### 개선 (3) <a href="#improvement-3" id="improvement-3"></a>

#### forEach 사용하기&#x20;

**`for문`** 대신 유사배열인 `.AccordionItem`을 배열화하여 **`forEach()` 메서드**를 사용합니다.

```typescript
const accordionItemNodeList = accordionNode.querySelectorAll('.AccordionItem')
const accordionArr = Array.from(accordionItemNodeList)

accordionArr.forEach(function (item) {
  item.addEventListener('click', handleToggleAccordionBody)
})

```

### 개선 (4) <a href="#improvement-4" id="improvement-4"></a>

`add()/ remove()` 메서드 대신 [classList.toggle() 메서드](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList#Examples)를 사용합니다. \
`classList.toggle()`는 인자에 전달된 클래스 이름의 유무를 확인하고 만약 있다면 선택된 요소에 제거하고 없다면 제거하는 기능을 하는 메서드입니다. \
즉, `toggle()` 메서드는 `contains()`, `add()`, `remove()` 메서드의 기능을 한 번에 수행합니다.&#x20;

```javascript
const handleToggleAccordionBody = function (e) {
  const accordionItemNode = e.target.closest('.AccordionItem')
  accordionItemNode.classList.toggle(ACCORDION_ITEM_ACTIVE_CLASS)
}
```

### 개선 (5) <a href="#improvement-5" id="improvement-5"></a>

#### Element.closest()에서 null이 반환되는 경우 오류를 방지하기

{% hint style="info" %}

#### Element.closest()

`closest()` 메서드는 `element node`에 사용이 가능한 메서드입니다. 메서드에 전달된 클래스 이름과 일치하는 가장 가까운 부모가 있을 때 부모 노드를 반환하는 메서드 입니다. 단, 일치하는 부모노드가 없을 경우 `null`을 반환합니다.&#x20;

`null`을 반환하기 때문에 조건문에서 사용이 가능합니다. \
null = false, 일치하는 노드가 있을 경우 = true 를 반환합니다.&#x20;
{% endhint %}

![null이 반환 됐을 때 오류 발생](/files/-MPisDN85ACN0KWTO8JU)

**논리 연산자를 사용하기**&#x20;

```javascript
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)
}
```

**조건문 사용하기**

```javascript
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 메서드에 전달된 인자를 확인하세요.'
    )
  }
}
```

## 결론  <a href="#conclude" id="conclude"></a>

```javascript
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)
})
```

## 주의 <a href="#warning" id="warning"></a>

* 메서드를 사용하기 전에 꼭 브라우저 호환성을 확인하기&#x20;
* `addEventListener`를 사용할 때는 `e.target`을 사용해서 이벤트가 걸려있는 요소 확인하기
* 코드를 작성하고 나서 리팩토링하는 작업을 꼭 한다.
  * 불필요한 코드(예, 주석, `console.log()` 등)을 정리하고 개선할 사항이 있는지 확인한다.&#x20;
