웹 개발을 하다 보면 버튼 클릭, 마우스 호버, 스크롤 등의 이벤트로 요소의 스타일을 동적으로 변경해야 하는 순간이 자주 온다. 대부분의 개발자들은 jQuery 시절부터 배운 방식대로 className을 문자열로 직접 조작하거나, 또는 classList 메서드를 제대로 이해하지 못한 채 복잡한 코드를 작성하고 있다. 이번에는 DOM 요소의 클래스를 안전하고 효율적으로 관리하는 정확한 방법을 실무 관점에서 정리해본다.

 

1단계. 클래스 조작의 세 가지 방식 이해하기

JavaScript에서 DOM 요소의 클래스를 다루는 방법은 크게 세 가지다. 가장 오래되고 위험한 방식부터 최신 방식까지 비교해보자.

방식 문법 장점 단점
className 직접 조작 element.className = "active" 간단함 기존 클래스 덮어쓰기, 공백 처리 복잡
classList 메서드 element.classList.add/remove/toggle 안전함, 기존 클래스 유지 구 브라우저(IE9 이하) 미지원
classList.toggle() element.classList.toggle("active") on/off 한 줄, 가독성 좋음 조건부 토글 시 두 번째 인자 필요

classList는 IE10 이상에서 지원되므로 현대적인 프로젝트라면 안심하고 써도 된다. 가장 중요한 점은 className으로 직접 문자열을 조작하면 기존 클래스가 모두 사라진다는 것이다.

 

2단계. 잘못된 코드 vs 올바른 코드
예제 1: 버튼 클릭 시 active 클래스 토글

✗ 잘못된 코드 (className으로 직접 조작)

const button = document.querySelector(".btn");
button.addEventListener("click", function() {
  if (button.className === "active") {
    button.className = "";
  } else {
    button.className = "active";
  }
});

이 코드는 버튼이 "btn btn-primary"라는 여러 클래스를 가지고 있다면 active를 추가할 때 "active"로만 덮어써진다. 기존 스타일이 모두 사라진다.

✓ 올바른 코드 (classList.toggle 사용)

const button = document.querySelector(".btn");
button.addEventListener("click", function() {
  button.classList.toggle("active");
});

한 줄이면 끝이다. 기존 클래스는 유지되고 active만 켜졌다 꺼졌다 한다.

 

예제 2: 조건에 따라 클래스 추가/제거

✗ 잘못된 코드 (if 문으로 일일이 조작)

const input = document.querySelector(".input");
const errorMsg = document.querySelector(".error");

input.addEventListener("blur", function() {
  if (input.value === "") {
    input.classList.add("error");
    errorMsg.classList.add("show");
  } else {
    input.classList.remove("error");
    errorMsg.classList.remove("show");
  }
});

동작하지만 조건이 복잡해질수록 코드가 중복된다.

✓ 올바른 코드 (classList.toggle의 두 번째 인자 활용)

const input = document.querySelector(".input");
const errorMsg = document.querySelector(".error");

input.addEventListener("blur", function() {
  const isEmpty = input.value === "";
  input.classList.toggle("error", isEmpty);
  errorMsg.classList.toggle("show", isEmpty);
});

classList.toggle의 두 번째 인자는 boolean이다. true면 클래스를 추가하고, false면 제거한다. 훨씬 간결하고 읽기 쉽다.

 

예제 3: 여러 요소의 클래스 일괄 조작

✗ 잘못된 코드 (for 루프로 일일이)

const tabs = document.querySelectorAll(".tab");
const contents = document.querySelectorAll(".content");

tabs.forEach(function(tab, index) {
  tab.addEventListener("click", function() {
    for (let i = 0; i < tabs.length; i++) {
      if (i === index) {
        tabs[i].classList.add("active");
        contents[i].classList.add("active");
      } else {
        tabs[i].classList.remove("active");
        contents[i].classList.remove("active");
      }
    }
  });
});

동작하지만 중복이 많고 실수하기 쉽다.

✓ 올바른 코드 (toggle의 두 번째 인자로 조건 판단)

const tabs = document.querySelectorAll(".tab");
const contents = document.querySelectorAll(".content");

tabs.forEach(function(tab, index) {
  tab.addEventListener("click", function() {
    tabs.forEach(function(t, i) {
      const isActive = i === index;
      t.classList.toggle("active", isActive);
      contents[i].classList.toggle("active", isActive);
    });
  });
});

forEach 안에서 toggle의 두 번째 인자로 조건을 직접 넘긴다. 코드가 깔끔하고 실수의 여지가 줄어든다.

 

3단계. classList의 모든 메서드 정리

classList가 제공하는 메서드는 toggle 외에도 여러 개다. 각각을 정확히 이해하면 상황에 맞는 최적의 선택을 할 수 있다.

const box = document.querySelector(".box");

// 클래스 추가 (이미 있으면 무시)
box.classList.add("active");

// 클래스 제거 (없으면 무시)
box.classList.remove("active");

// 클래스 토글 (없으면 추가, 있으면 제거)
box.classList.toggle("active");

// 조건에 따라 토글 (true면 추가, false면 제거)
box.classList.toggle("active", condition);

// 클래스 포함 여부 확인 (true/false 반환)
if (box.classList.contains("active")) {
  console.log("active 클래스가 있다");
}

// 모든 클래스 조회 (DOMTokenList 객체 반환)
console.log(box.classList);
// 결과: DOMTokenList { 0: 'box', 1: 'active', length: 2, ... }

// replace로 클래스 교체 (IE/구 브라우저 미지원)
box.classList.replace("active", "inactive");

 

4단계. 실전 예제: 토글 가능한 아코디언

실제 웹 프로젝트에서 자주 쓰는 아코디언을 만들어보자.

<div class="accordion">
  <div class="accordion-item">
    <button class="accordion-btn">섹션 1</button>
    <div class="accordion-content">
      

섹션 1 내용

</div> </div> <div class="accordion-item"> <button class="accordion-btn">섹션 2</button> <div class="accordion-content">

섹션 2 내용

</div> </div> </div>
.accordion-content {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.3s ease;
}

.accordion-content.active {
  max-height: 500px;
}

.accordion-btn.active {
  background-color: #333;
  color: #fff;
}
const buttons = document.querySelectorAll(".accordion-btn");

buttons.forEach(function(btn) {
  btn.addEventListener("click", function() {
    const content = btn.nextElementSibling;
    const isActive = btn.classList.contains("active");
    
    // 다른 모든 아코디언 닫기
    buttons.forEach(function(b) {
      b.classList.remove("active");
      b.nextElementSibling.classList.remove("active");
    });
    
    // 클릭한 버튼이 이미 열려있었다면 닫기, 아니면 열기
    if (!isActive) {
      btn.classList.add("active");
      content.classList.add("active");
    }
  });
});

이 코드는 한 번에 하나의 아코디언만 열 수 있고, 다시 클릭하면 닫히는 일반적인 아코디언 패턴이다.

 

5단계. 주의사항과 성능 최적화

className으로 직접 조작하는 것은 반드시 피하자. 특히 반복문 안에서 여러 요소의 클래스를 자주 변경한다면 성능에 영향을 줄 수 있다. classList 메서드는 각 호출마다 DOM을 재계산하므로, 같은 요소에 여러 클래스를 추가할 때는 가능하면 묶어서 처리하자.

✗ 비효율적인 코드 (여러 번 리플로우)

const box = document.querySelector(".box");
box.classList.add("active");
box.classList.add("visible");
box.classList.add("animated");

✓ 효율적인 코드 (한 번에 여러 클래스 추가)

const box = document.querySelector(".box");
box.classList.add("active", "visible", "animated");

classList.add와 remove는 여러 인자를 받을 수 있다. 이렇게 하면 브라우저가 한 번의 리페인트만 실행한다.

 

결론

classList는 현대적인 JavaScript 개발의 필수 도구다. toggle의 두 번째 인자를 제대로 이해하면 조건부 클래스 추가/제거 코드를 훨씬 간결하게 쓸 수 있다. 불필요한 if 문을 줄이고, className 직접 조작의 위험을 완벽히 피하는 것만으로도 코드의 안정성과 가독성이 한 단계 올라간다. 이 글의 toggle 활용 패턴을 의식적으로 반복하다 보면, UI 상태 관리가 훨씬 직관적이 될 것이다.