[UI component] accordion 2

·2025년 4월 6일
0

[Study] Toy project

목록 보기
3/4

필요한 개념

classList.add()
classList.remove()
classList.toggle()
classList.contains()

지난 아코디언 과제 때는 depth2가 존재하는 경우에만 클릭 시 패널이 열리고 닫히는 기능까지 구현했다. 하지만 하나의 패널을 열면 다른 패널은 모두 닫히지 않고 클릭한 패널이 동시에 모두 열리는 구조라 이 부분을 수정해보기로 했다.



1. 수정 전 기존 코드 (패널 여닫기 가능, 중복으로 열림)

📄 menuAcc.html

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Menu Accordion</title>
    <link
      href="https://hangeul.pstatic.net/hangeul_static/css/nanum-gothic.css"
      rel="stylesheet"
    />
    <link
      rel="stylesheet"
      href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&icon_names=keyboard_arrow_down"
    />
    <link rel="stylesheet" href="../resources/css/output/menuAccStyle.css" />
    <script defer src="../resources/js/menuAcc.js"></script>
  </head>
  <body>
    <main>
      <div class="container">
        <ul class="menu-box">
          <li class="active"><a href="#">첫번째 메뉴</a></li>
          <li><a href="#">두번째 메뉴</a></li>
          <li>
            <a href="#"
              >세번째 메뉴
              <span class="material-symbols-outlined"
                >keyboard_arrow_down</span
              ></a
            >
            <ul class="depth2">
              <li><a href="#">메뉴 3-1</a></li>
              <li><a href="#">메뉴 3-2</a></li>
              <li><a href="#">메뉴 3-3</a></li>
            </ul>
          </li>
          <li>
            <a href="#"
              >네번째 메뉴
              <span class="material-symbols-outlined">
                keyboard_arrow_down
              </span>
            </a>
            <ul class="depth2">
              <li><a href="#">메뉴 4-1</a></li>
              <li><a href="#">메뉴 4-2</a></li>
              <li><a href="#">메뉴 4-3</a></li>
            </ul>
          </li>
        </ul>
      </div>
    </main>
  </body>
</html>



🎨 menuAcc.scss


* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  text-decoration: none;
  list-style: none;
  color: rgb(182, 189, 193);
  font-family: "NanumGothic";
}

.menu-box {
  display: inline-block;
  background-color: rgb(12, 27, 33);
}

li {
  a {
    display: flex;
    justify-content: space-between;
    align-items: center;
    height: 56px;
    margin-left: 14px;
    padding: 18px 0;
    span {
      margin-left: 14px;
      margin-right: 10px;
    }
  }
}

.depth2 {
  display: none;
}

li.active {
  .depth2 {
    display: block;
  }
}

.hover {
  background-color: rgb(59, 73, 81);
}

.active {
  background-color: rgb(41, 56, 64);
  color: rgb(248, 248, 248);
  font-weight: bold;
}



⚙️ menuAcc.js

document.addEventListener("click", (e) => {
  const aTag = e.target.closest("a");

  if (!aTag) return;

  if (aTag.querySelector("span")) {
    const li = aTag.closest("li");
    if (li.querySelector(".depth2")) {
      // classList.add / remove 사용법
      //   if (li.classList.contains("active")) {
      //     li.classList.remove("active");
      //   } else {
      //     li.classList.add("active");
      //   }
      // }

      // classList.toggle 사용법
      li.classList.toggle("active", !li.classList.contains("active"));
    }
  }
});

처음에는 가장 익숙했던 classList.add와 remove를 사용해 구현했는데, 찾아보니 toggle 메소드가 있었고 toggle 사용 시 더 간단하게 구현할 수 있을 것 같아 toggle을 사용한 버전으로도 구현해 보았다.





2. 수정 후 코드 (패널이 열리면 다른 패널 모두 닫힘)

js 코드만 일부 수정 후 메뉴 클릭 시 패널이 열리고, 이외의 패널은 닫히도록 구현했다.

const liTags = document.querySelectorAll("li");

document.addEventListener("click", (e) => {
  const aTag = e.target.closest("a");

  if (!aTag) return;

  if (aTag.querySelector("span")) {
    const li = aTag.closest("li");
    if (li.querySelector(".depth2")) {
      liTags.forEach((item) => {
        item.classList.remove("active");
        if (item === li) {
          item.classList.toggle("active", !item.classList.contains("active"));
        }
      });
    }
  }
});

그런데 여전히 한 가지 문제가 있었다. 이젠 패널이 열린 메뉴를 클릭하면 다시 접히는 게 안 됐다!

2-1. 🛠️ Trouble shooting

음..모든 li에서 active를 제거하고 토글을 적용하면 돼야 하는 거 아닌가?
혼자서는 밤새 고민해도 답을 찾지 못할 것 같아서 챗GPT의 힘을 빌렸다.
그 결과 원인과 해결 방법은 아주 간단했다!

🔽 문제 코드는 이 부분이었다.

liTags.forEach((item) => {
  item.classList.remove("active");
  if (item === li) {
    item.classList.toggle("active", !item.classList.contains("active"));
  }
});

메뉴 클릭 시 다른 패널들을 모두 닫기 위해 active클래스를 모두 제거하는 접근 방식 자체는 나쁘지 않았으나 순서가 문제였다.

item.classList.remove("active")를 먼저 호출하기 때문에 다음 줄의 item.classList.contains("active")항상 false가 되는 것이 문제였다 ! !

즉, 클릭한 li가 이미 active더라도,

  • 먼저 .remove("active") 해버려서
  • 아래에서 .toggle("active", true)가 실행되면서
  • 결과적으로 다시 active가 붙어버려

토글이 작동하지 않고 항상 열려버리는 것처럼 보이는 거였다.


✅ 해결 방법

forEach를 돌리기 전에, 현재 클릭한 li가 active 상태인지 먼저 따로 저장해두고,
그걸 기준으로 toggle을 적용시키면 된다고 하길래 적용해 보았습니다.
GPT가 제공한 코드를 그대로 복붙하는 건 싫어서 대강 훑어보기만 하고 스스로 코드를 적어봤는데 원하는대로 잘 작동되긴 했지만 GPT가 추천해 준 코드와 조금 다른 점이 있었다.

내가 작성한 코드도 원하는 대로 패널 하나가 열리면 다른 패널은 닫히는 기능이 구현되어서 둘 중 어떤 코드가 더 좋은 코드인지 궁금했다.

결론은 성능적, 가독성 측면에서 GPT의 추천 코드가 더 좋은 코드라고 생각되었다.


🤔 내 코드 :

const liTags = document.querySelectorAll("li");

document.addEventListener("click", (e) => {
  const aTag = e.target.closest("a");

  if (!aTag) return;

  if (aTag.querySelector("span")) {
    const li = aTag.closest("li");
    if (li.querySelector(".depth2")) {
      liTags.forEach((item) => {
        const isActive = item.classList.contains("active");

        item.classList.remove("active");
        if (item === li) {
          item.classList.toggle("active", !isActive);
        }
      });
    }
  }
});
  • 🔁 forEach를 돌면서 모든 li의 활성 상태를 검사함
  • 실제로는 li === item인 경우에만 isActive가 필요한데, 모든 요소에서 contains 체크를 하고 있음

🤖 GPT의 코드

document.addEventListener("click", (e) => {
  const aTag = e.target.closest("a");

  if (!aTag) return;

  if (aTag.querySelector("span")) {
    const li = aTag.closest("li");
    if (li.querySelector(".depth2")) {
      const isActive = li.classList.contains("active"); // 현재 상태 저장

      liTags.forEach((item) => item.classList.remove("active"));

      if (!isActive) {
        li.classList.add("active"); // 기존에 열려있던 게 아니면 열기
      }
    }
  }
});
  • 클릭한 li 하나만 상태 체크 (li.classList.contains("active"))
  • 이후 전체 liremove("active") 하고, 조건에 따라 단 한 곳만 add("active")
  • 반복 안에서 조건 판단이 적고 명확함 → 가독성, 성능 측면에서 더 깔끔

💡 결론

기준네 코드추천 코드
동작✅ 정상 작동✅ 정상 작동
가독성🔸 조금 복잡✅ 더 간결하고 명확
성능🔸 약간 더 연산 많음✅ 최소한의 연산

규모가 작으면 성능 차이는 거의 없지만 협업하거나 유지보수를 고려하면, 명확하게 목적이 드러나는 구조가 더 좋다는 점에서 GPT가 추천한 코드가 더 권장되는 스타일이라고 한다.


🔁 정리

  • 기존 코드: 항상 .remove() → 무조건 다시 열림
  • 수정 코드: 클릭한 li가 열려 있었는지 체크 → 그 상태에 따라 열거나 접음



🌟 결과

0개의 댓글