필요한 개념
classList.add()
classList.remove()
classList.toggle()
classList.contains()
지난 아코디언 과제 때는 depth2가 존재하는 경우에만 클릭 시 패널이 열리고 닫히는 기능까지 구현했다. 하지만 하나의 패널을 열면 다른 패널은 모두 닫히지 않고 클릭한 패널이 동시에 모두 열리는 구조라 이 부분을 수정해보기로 했다.
📄 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을 사용한 버전으로도 구현해 보았다.
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"));
}
});
}
}
});
그런데 여전히 한 가지 문제가 있었다. 이젠 패널이 열린 메뉴를 클릭하면 다시 접히는 게 안 됐다!
음..모든 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)
가 실행되면서토글이 작동하지 않고 항상 열려버리는 것처럼 보이는 거였다.
✅ 해결 방법
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")
)li
를 remove("active")
하고, 조건에 따라 단 한 곳만 add("active")
💡 결론
기준 | 네 코드 | 추천 코드 |
---|---|---|
동작 | ✅ 정상 작동 | ✅ 정상 작동 |
가독성 | 🔸 조금 복잡 | ✅ 더 간결하고 명확 |
성능 | 🔸 약간 더 연산 많음 | ✅ 최소한의 연산 |
규모가 작으면 성능 차이는 거의 없지만 협업하거나 유지보수를 고려하면, 명확하게 목적이 드러나는 구조가 더 좋다는 점에서 GPT가 추천한 코드가 더 권장되는 스타일이라고 한다.
🔁 정리
.remove()
→ 무조건 다시 열림