아코디언 메뉴 (Width 방식) - 완전 정복
📋 목차

개요
이 코드는 width 속성을 조절하여 아코디언 메뉴를 구현한 예제입니다. dt 요소를 클릭하면 해당 dd 요소의 너비를 조절하여 열고 닫는 효과를 만듭니다.
주요 특징:
dd요소의width를0px와695px로 조절- 한 번에 하나의 메뉴만 열림 (다른 메뉴는 자동으로 닫힘)
- 선택된
dt의span에selected클래스 추가 - 첫 번째 메뉴가 기본적으로 열려있음
코드 분석
원본 코드 (for 루프 사용)
document.addEventListener('DOMContentLoaded', function(){
var ddList = document.querySelectorAll('dd');
var dtList = document.querySelectorAll('dl dt');
var dtListSpan = document.querySelectorAll('dl dt span');
// 1. 첫 번째를 제외한 모든 dd를 닫기
for (var i = 1; i < ddList.length; i++) {
ddList[i].style.width = '0px';
}
// 2. 첫 번째 dt의 span에 selected 클래스 추가
dtListSpan[0].classList.add('selected');
// 3. 각 dt에 클릭 이벤트 리스너 추가
for(var i = 0; i < dtList.length; i++){
dtList[i].addEventListener('click', function(){
var nextDD = this.nextElementSibling;
// 4. 클릭한 dd가 닫혀있으면 모든 dd 닫기
if(nextDD.style.width == '0px' || nextDD.style.width === ''){
for(var j = 0; j < ddList.length; j++){
ddList[j].style.width = '0px';
}
}
// 5. 클릭한 dd 열기
nextDD.style.width = "695px";
// 6. 모든 span에서 selected 제거
for(var k = 0; k < dtList.length; k++){
dtListSpan[k].classList.remove('selected');
}
// 7. 클릭한 dt의 span에 selected 추가
this.querySelector('span').classList.add('selected');
});
}
});
forEach로 변환된 코드
document.addEventListener('DOMContentLoaded', function(){
var ddList = document.querySelectorAll('dd');
var dtList = document.querySelectorAll('dl dt');
var dtListSpan = document.querySelectorAll('dl dt span');
// 1. 첫 번째를 제외한 모든 dd를 닫기
ddList.forEach(function(dd, index) {
if (index !== 0) {
dd.style.width = '0px';
}
});
// 2. 첫 번째 dt의 span에 selected 클래스 추가
dtListSpan[0].classList.add('selected');
// 3. 각 dt에 클릭 이벤트 리스너 추가
dtList.forEach(function(dt) {
dt.addEventListener('click', function(){
var nextDD = this.nextElementSibling;
// 4. 클릭한 dd가 닫혀있으면 모든 dd 닫기
if(nextDD.style.width == '0px' || nextDD.style.width === ''){
ddList.forEach(function(dd) {
dd.style.width = '0px';
});
}
// 5. 클릭한 dd 열기
nextDD.style.width = "695px";
// 6. 모든 span에서 selected 제거
dtListSpan.forEach(function(span) {
span.classList.remove('selected');
});
// 7. 클릭한 dt의 span에 selected 추가
this.querySelector('span').classList.add('selected');
});
});
});
ES6 화살표 함수 버전 (더 간결)
document.addEventListener('DOMContentLoaded', () => {
const ddList = document.querySelectorAll('dd');
const dtList = document.querySelectorAll('dl dt');
const dtListSpan = document.querySelectorAll('dl dt span');
// 1. 첫 번째를 제외한 모든 dd를 닫기
ddList.forEach((dd, index) => {
if (index !== 0) {
dd.style.width = '0px';
}
});
// 2. 첫 번째 dt의 span에 selected 클래스 추가
dtListSpan[0].classList.add('selected');
// 3. 각 dt에 클릭 이벤트 리스너 추가
dtList.forEach(dt => {
dt.addEventListener('click', function(){
const nextDD = this.nextElementSibling;
// 4. 클릭한 dd가 닫혀있으면 모든 dd 닫기
if(nextDD.style.width == '0px' || nextDD.style.width === ''){
ddList.forEach(dd => {
dd.style.width = '0px';
});
}
// 5. 클릭한 dd 열기
nextDD.style.width = "695px";
// 6. 모든 span에서 selected 제거
dtListSpan.forEach(span => {
span.classList.remove('selected');
});
// 7. 클릭한 dt의 span에 selected 추가
this.querySelector('span').classList.add('selected');
});
});
});
라인별 상세 설명
1. DOMContentLoaded 이벤트 리스너
document.addEventListener('DOMContentLoaded', function(){
설명:
DOMContentLoaded: HTML 문서가 완전히 로드되고 파싱된 후 실행- DOM 요소를 선택하기 전에 문서가 준비되어야 함
- 스크립트가
<head>에 있어도 안전하게 실행 가능
2. DOM 요소 선택
var ddList = document.querySelectorAll('dd');
var dtList = document.querySelectorAll('dl dt');
var dtListSpan = document.querySelectorAll('dl dt span');
요소 설명:
| 변수 | 선택자 | 설명 |
|---|---|---|
ddList |
'dd' |
모든 dd 요소들 (아코디언 내용 영역) |
dtList |
'dl dt' |
모든 dt 요소들 (아코디언 제목) |
dtListSpan |
'dl dt span' |
모든 dt 내부의 span 요소들 |
HTML 구조 예시:
<dl>
<dt><span>Step.1</span></dt>
<dd>내용...</dd>
<dt><span>Step.2</span></dt>
<dd>내용...</dd>
</dl>
3. 초기화: 첫 번째를 제외한 모든 dd 닫기
원본 코드 (for 루프)
for (var i = 1; i < ddList.length; i++) {
ddList[i].style.width = '0px';
}
forEach 버전
ddList.forEach(function(dd, index) {
if (index !== 0) {
dd.style.width = '0px';
}
});
설명:
forEach(): 배열/NodeList의 각 요소에 대해 함수 실행dd: 현재 처리 중인dd요소index: 현재 인덱스 (0부터 시작)index !== 0: 첫 번째(index === 0)를 제외한 모든ddwidth = '0px': 너비를 0으로 설정하여 숨김
왜 첫 번째를 제외하나요?
- 첫 번째 메뉴는 기본적으로 열려있어야 함
- 사용자가 페이지를 열었을 때 첫 번째 메뉴가 보이도록 함
4. 첫 번째 dt의 span에 selected 클래스 추가
dtListSpan[0].classList.add('selected');
설명:
dtListSpan[0]: 첫 번째span요소classList.add('selected'):selected클래스 추가- CSS에서
.selected클래스로 선택된 메뉴의 스타일 적용
5. 각 dt에 클릭 이벤트 리스너 추가
원본 코드 (for 루프)
for(var i = 0; i < dtList.length; i++){
dtList[i].addEventListener('click', function(){
// ...
});
}
forEach 버전
dtList.forEach(function(dt) {
dt.addEventListener('click', function(){
// ...
});
});
설명:
forEach(): 각dt요소에 대해 반복addEventListener('click', ...): 클릭 이벤트 리스너 추가function(): 이벤트 핸들러 함수 (일반 함수 사용 →this가dt요소를 가리킴)
왜 일반 함수를 사용하나요?
- 화살표 함수(
=>)를 사용하면this가dt요소를 가리키지 않음 - 일반 함수를 사용하면
this가 클릭한dt요소를 가리킴
6. 클릭한 dd가 닫혀있는지 확인
var nextDD = this.nextElementSibling;
if(nextDD.style.width == '0px' || nextDD.style.width === ''){
// 모든 dd 닫기
}
설명:
this.nextElementSibling: 클릭한dt의 다음 형제 요소 (dd)nextElementSibling: 같은 부모를 가진 다음 요소nextDD.style.width == '0px': 인라인 스타일로width가0px인지 확인nextDD.style.width === '': 인라인 스타일이 없거나 빈 문자열인지 확인
조건문의 의미:
dd가 닫혀있으면 (width === '0px'또는width === '')- 모든
dd를 먼저 닫고 - 클릭한
dd만 열기
왜 이렇게 하나요?
- 한 번에 하나의 메뉴만 열리도록 하기 위함
- 다른 메뉴가 열려있으면 먼저 닫고 새 메뉴 열기
7. 모든 dd 닫기
원본 코드 (for 루프)
for(var j = 0; j < ddList.length; j++){
ddList[j].style.width = '0px';
}
forEach 버전
ddList.forEach(function(dd) {
dd.style.width = '0px';
});
설명:
- 모든
dd요소를 순회하며 width를0px로 설정하여 닫기
8. 클릭한 dd 열기
nextDD.style.width = "695px";
설명:
- 클릭한
dt의 다음dd요소의 너비를695px로 설정 695px: 메뉴가 열렸을 때의 너비 (디자인에 따라 조절 가능)
9. 모든 span에서 selected 제거
원본 코드 (for 루프)
for(var k = 0; k < dtList.length; k++){
dtListSpan[k].classList.remove('selected');
}
forEach 버전
dtListSpan.forEach(function(span) {
span.classList.remove('selected');
});
설명:
- 모든
span요소를 순회하며 selected클래스를 제거하여 이전 선택 상태 해제
10. 클릭한 dt의 span에 selected 추가
this.querySelector('span').classList.add('selected');
설명:
this: 클릭한dt요소querySelector('span'):dt내부의span요소 찾기classList.add('selected'):selected클래스 추가- CSS에서
.selected클래스로 선택된 메뉴의 스타일 적용
실행 흐름
시나리오 1: 페이지 로드 시
1. DOMContentLoaded 이벤트 발생
2. ddList, dtList, dtListSpan 선택
3. 첫 번째를 제외한 모든 dd의 width를 0px로 설정
- ddList[1].style.width = '0px'
- ddList[2].style.width = '0px'
- ...
4. 첫 번째 dt의 span에 selected 클래스 추가
5. 각 dt에 클릭 이벤트 리스너 추가결과:
- 첫 번째 메뉴만 열려있음
- 첫 번째 메뉴의
dt에selected클래스가 적용됨
시나리오 2: 두 번째 메뉴 클릭 시
1. 사용자가 두 번째 dt 클릭
2. nextDD = 두 번째 dd 요소
3. nextDD.style.width 확인
- '0px' 또는 ''이므로 조건 만족
4. 모든 dd의 width를 0px로 설정
- 첫 번째 dd 닫힘
- 두 번째 dd 닫힘 (곧바로 열릴 예정)
- 세 번째 dd 닫힘
5. nextDD.style.width = "695px" 설정
- 두 번째 dd 열림
6. 모든 span에서 selected 제거
7. 두 번째 dt의 span에 selected 추가결과:
- 두 번째 메뉴만 열려있음
- 두 번째 메뉴의
dt에selected클래스가 적용됨
시나리오 3: 같은 메뉴를 다시 클릭 시
1. 사용자가 이미 열려있는 두 번째 dt 클릭
2. nextDD = 두 번째 dd 요소
3. nextDD.style.width 확인
- '695px'이므로 조건 불만족
4. 모든 dd 닫기 단계 건너뜀
5. nextDD.style.width = "695px" 설정 (이미 695px)
6. 모든 span에서 selected 제거
7. 두 번째 dt의 span에 selected 추가결과:
- 두 번째 메뉴가 계속 열려있음 (변화 없음)
⚠️ 문제점:
- 같은 메뉴를 다시 클릭해도 닫히지 않음
- 개선이 필요할 수 있음
핵심 개념
1. forEach() 메서드
array.forEach(function(element, index, array) {
// 각 요소에 대해 실행할 코드
});
매개변수:
element: 현재 처리 중인 요소index: 현재 인덱스 (선택적)array: 원본 배열 (선택적)
장점:
- 코드가 더 읽기 쉬움
- 인덱스 관리 불필요
- 실수로 인덱스 범위를 벗어날 위험 없음
for 루프 vs forEach:
// for 루프
for(var i = 0; i < array.length; i++){
console.log(array[i]);
}
// forEach
array.forEach(function(item) {
console.log(item);
});
2. nextElementSibling
var nextDD = this.nextElementSibling;
설명:
- 같은 부모를 가진 다음 형제 요소를 반환
dt의 다음 형제는dd요소
HTML 구조:
<dl>
<dt>제목</dt> ← this
<dd>내용</dd> ← nextElementSibling
</dl>
3. style.width 조절
dd.style.width = '0px'; // 닫기
dd.style.width = '695px'; // 열기
설명:
- 인라인 스타일로 직접
width속성 설정 - CSS transition과 함께 사용하면 애니메이션 효과 가능
주의사항:
style.width는 인라인 스타일만 확인- CSS 파일에서 설정한
width는 확인하지 않음 window.getComputedStyle()을 사용하면 CSS 파일의 스타일도 확인 가능
4. classList 메서드
element.classList.add('selected'); // 클래스 추가
element.classList.remove('selected'); // 클래스 제거
element.classList.toggle('selected'); // 클래스 토글
설명:
classList: 요소의 클래스 목록을 관리하는 객체add(): 클래스 추가remove(): 클래스 제거toggle(): 클래스가 있으면 제거, 없으면 추가
5. 클로저와 this 바인딩
dtList.forEach(function(dt) {
dt.addEventListener('click', function(){
// this는 클릭한 dt 요소를 가리킴
var nextDD = this.nextElementSibling;
});
});
설명:
- 일반 함수(
function())를 사용하면this가 이벤트 타겟을 가리킴 - 화살표 함수(
=>)를 사용하면this가 상위 스코프를 가리킴
화살표 함수 사용 시 문제:
dtList.forEach(dt => {
dt.addEventListener('click', () => {
// this가 dt를 가리키지 않음!
var nextDD = this.nextElementSibling; // ❌ 오류
});
});
개선 가능한 부분
1. 같은 메뉴를 다시 클릭하면 닫히도록 개선
dtList.forEach(function(dt) {
dt.addEventListener('click', function(){
const nextDD = this.nextElementSibling;
const isOpen = nextDD.style.width === '695px';
if (isOpen) {
// 이미 열려있으면 닫기
nextDD.style.width = '0px';
this.querySelector('span').classList.remove('selected');
} else {
// 닫혀있으면 열기
ddList.forEach(function(dd) {
dd.style.width = '0px';
});
nextDD.style.width = "695px";
dtListSpan.forEach(function(span) {
span.classList.remove('selected');
});
this.querySelector('span').classList.add('selected');
}
});
});
2. CSS transition 추가로 애니메이션 효과
dd {
transition: width 0.3s ease;
overflow: hidden;
}
3. window.getComputedStyle() 사용
const computedStyle = window.getComputedStyle(nextDD);
if (computedStyle.width === '0px' || computedStyle.width === '695px') {
// ...
}
마무리
이 코드는 width 속성을 조절하여 아코디언 메뉴를 구현한 간단한 예제입니다. forEach()를 사용하면 코드가 더 읽기 쉽고 안전해집니다.
핵심 학습 포인트:
- ✅
forEach()메서드의 활용 - ✅
nextElementSibling으로 형제 요소 찾기 - ✅
style.width로 인라인 스타일 조절 - ✅
classList로 클래스 관리 - ✅ 클로저와
this바인딩 이해
이 코드를 이해하면 다양한 아코디언 메뉴 구현이 가능합니다!
'FrontEnd > Javascript' 카테고리의 다른 글
| 이미지 슬라이더 무한 루프 - javascript (0) | 2026.02.21 |
|---|---|
| 세로메뉴 - 아코디언 메뉴 (1) | 2026.02.16 |
| 아코디언 메뉴 (Height + CSS Transition 방식) (0) | 2026.02.13 |
| JavaScript 익명 함수와 화살표 함수 (0) | 2026.02.13 |
| 모바일 메뉴(햄버거메뉴) - 네비 (0) | 2026.02.12 |