DOM 요소의 클래스를 추가하거나 제거할 때, 대부분의 개발자는 직접 className 문자열을 조작하거나 jQuery의 addClass/removeClass를 써왔을 것이다. 다만 현대 자바스크립트에서 이렇게 하는 것은 번들 크기 낭비이고, 실수하기 쉬운 방식이다.
이번에는 classList API가 정확히 무엇인지, 왜 className 직접 조작보다 우월한지, 실무에서 어떻게 활용하는지 완벽하게 정리해서 소개하겠다.

 

1단계: classList API 기초 이해하기
classList는 모든 DOM 요소가 가진 읽기 전용 속성으로, DOMTokenList라는 특수한 객체를 반환한다. 이 객체는 요소의 클래스들을 배열처럼 다룰 수 있게 해주는 메서드들을 제공한다.

classList가 제공하는 주요 메서드:
add() - 하나 이상의 클래스 추가
remove() - 하나 이상의 클래스 제거
toggle() - 클래스가 있으면 제거, 없으면 추가
contains() - 특정 클래스 보유 여부 확인
replace() - 기존 클래스를 새 클래스로 교체

classList는 자동으로 공백을 처리해주고, 중복 클래스 추가를 막으며, 숫자 인덱싱도 지원한다. 이는 className 문자열 조작으로는 복잡한 작업들을 간단하게 만들어준다.

 

2단계: className 직접 조작 vs classList - 문제점 비교
작업 ❌ className 직접 조작 ✓ classList 사용
클래스 추가 el.className += ' active' (공백 처리 위험) el.classList.add('active') (안전)
클래스 제거 el.className = el.className.replace('active', '') (복잡) el.classList.remove('active') (간단)
클래스 토글 el.classList.contains('active') ? el.className = ... : el.className = ... (장황) el.classList.toggle('active') (우아함)
클래스 존재 확인 el.className.includes('active') (부분 문자열 오류 가능) el.classList.contains('active') (정확)
복수 클래스 추가 el.className += ' btn btn-primary' (중복 체크 없음) el.classList.add('btn', 'btn-primary') (자동 중복 방지)

 

3단계: 실전 예제 - classList 메서드별 사용법
예제 1: add() - 클래스 추가

❌ 잘못된 코드:

const btn = document.getElementById('myBtn');
// 문제: 이미 'active'가 있으면 중복으로 추가됨
btn.className = btn.className + ' active';
console.log(btn.className); // "btn primary active active" (중복!)

✓ 올바른 코드:

const btn = document.getElementById('myBtn');
// 해결: 중복 자동 방지, 공백 자동 처리
btn.classList.add('active');
btn.classList.add('disabled', 'highlight'); // 여러 개 한 번에 추가
console.log(btn.className); // "btn primary active disabled highlight"

결과: classList.add()는 이미 존재하는 클래스를 중복으로 추가하지 않고, 공백 처리를 자동으로 한다.

예제 2: remove() - 클래스 제거

❌ 잘못된 코드:

const el = document.querySelector('.modal');
// 문제: 'active'와 비슷한 이름('inactive' 등)도 함께 제거될 수 있음
el.className = el.className.replace('active', '');
console.log(el.className); // "modal inactive" (의도하지 않은 제거!)

✓ 올바른 코드:

const el = document.querySelector('.modal');
// 해결: 정확한 클래스만 제거
el.classList.remove('active');
el.classList.remove('hidden', 'faded'); // 여러 개 한 번에 제거
console.log(el.className); // "modal"

결과: classList.remove()는 토큰 단위로 정확하게 제거하므로 부분 문자열 오류가 없다.

예제 3: toggle() - 클래스 추가/제거 자동화

❌ 잘못된 코드:

// 메뉴 열기/닫기 버튼
const menuBtn = document.getElementById('menuBtn');
menuBtn.addEventListener('click', function() {
  // 문제: 클래스 존재 확인, 추가/제거 로직이 장황함
  if (menuBtn.className.includes('open')) {
    menuBtn.className = menuBtn.className.replace('open', '');
  } else {
    menuBtn.className += ' open';
  }
});

✓ 올바른 코드:

const menuBtn = document.getElementById('menuBtn');
menuBtn.addEventListener('click', function() {
  // 해결: 한 줄로 자동 처리
  menuBtn.classList.toggle('open');
});

결과: 'open' 클래스가 있으면 제거, 없으면 추가된다. 코드는 간결하고 읽기 쉽다.

예제 4: contains() - 클래스 존재 확인

❌ 잘못된 코드:

const card = document.querySelector('.card');
// 문제: 'active'가 포함된 다른 클래스명도 true 반환
if (card.className.includes('active')) {
  console.log('카드가 활성화됨'); // "inactive"도 활성화로 판단!
}

✓ 올바른 코드:

const card = document.querySelector('.card');
// 해결: 정확한 토큰 확인
if (card.classList.contains('active')) {
  console.log('카드가 활성화됨'); // 'active'만 확인
}

결과: 'active'라는 정확한 클래스만 검사한다. 'inactive', 'activated' 등은 false를 반환한다.

예제 5: replace() - 클래스 교체

❌ 잘못된 코드:

const btn = document.querySelector('button');
// 문제: 두 번의 작업 필요
btn.classList.remove('btn-primary');
btn.classList.add('btn-secondary');

✓ 올바른 코드:

const btn = document.querySelector('button');
// 해결: 한 번에 교체
btn.classList.replace('btn-primary', 'btn-secondary');
console.log(btn.className); // "btn btn-secondary"

결과: 'btn-primary'를 찾아 'btn-secondary'로 교체한다. 한 번의 작업으로 효율적이다.

 

4단계: 주의사항 및 흔한 실수
실수 1: className과 classList를 혼동

❌ 잘못된 코드:

// className은 전체 클래스 문자열을 반환 (읽기/수정 모두 가능)
const str = el.className; // "btn primary active"

// classList는 DOMTokenList 객체를 반환 (읽기 전용)
const list = el.classList; // DOMTokenList(3) ['btn', 'primary', 'active']

// 이렇게 하면 안 됨:
const val = el.classList = 'new-class'; // TypeError!

✓ 올바른 방법:

// 기존 클래스 모두 제거 후 새로 지정하려면:
el.className = 'new-class'; // 또는
el.setAttribute('class', 'new-class');
실수 2: toggle() 두 번째 인자 모르기

❌ 기본 사용만 알고 있는 코드:

// toggle은 상태를 강제로 지정할 수 없음
const isEnabled = true;
el.classList.toggle('enabled'); // true든 false든 무조건 토글됨

✓ 강제 지정 방법:

// toggle의 두 번째 인자로 강제 지정 (true면 추가, false면 제거)
const isEnabled = true;
el.classList.toggle('enabled', isEnabled); // true이므로 무조건 추가

const isVisible = false;
el.classList.toggle('visible', isVisible); // false이므로 무조건 제거
실수 3: IE 11 이하 호환성 무시

❌ 주의할 점:

// classList는 IE 10 이상에서 지원
// IE 9 이하에서는 작동하지 않음
if (!el.classList) {
  // IE 9 이하 폴백 처리 필요
  el.className += ' active';
}

✓ 현대 프로젝트 가정 (IE 지원 안 함):

// 최신 브라우저만 지원한다면 그냥 사용
el.classList.add('active');
실수 4: 너무 많은 클래스 동시 조작

❌ 비효율적인 코드:

// 매번 DOM 리플로우 유발
button.classList.add('loading');
button.classList.add('disabled');
button.classList.add('spinner');
button.classList.remove('hover');

✓ 효율적인 방법:

// 한 번에 처리 (DOM 리플로우 1회)
button.classList.add('loading', 'disabled', 'spinner');
button.classList.remove('hover');

// 또는 CSS에 복합 상태 클래스 정의
// .button.loading { /* 여러 스타일 한 번에 */ }

 

5단계: 실무 응용 - 실제 사용 시나리오
시나리오: 모달 다이얼로그 제어
class Modal {
  constructor(selector) {
    this.el = document.querySelector(selector);
    this.backdrop = document.querySelector('.modal-backdrop');
  }

  open() {
    this.el.classList.add('show');
    this.el.classList.remove('hide');
    this.backdrop.classList.add('visible');
    document.body.classList.add('modal-open'); // 스크롤 잠금
  }

  close() {
    this.el.classList.remove('show');
    this.el.classList.add('hide');
    this.backdrop.classList.remove('visible');
    document.body.classList.remove('modal-open');
  }

  toggle() {
    this.el.classList.toggle('show');
  }

  isOpen() {
    return this.el.classList.contains('show');
  }
}

// 사용 예
const modal = new Modal('#myModal');
document.getElementById('openBtn').addEventListener('click', () => modal.open());
document.getElementById('closeBtn').addEventListener('click', () => modal.close());
시나리오: 폼 검증 UI
function validateInput(inputEl) {
  const value = inputEl.value.trim();
  const isValid = value.length >= 3;

  // 기존 상태 클래스 제거
  inputEl.classList.remove('is-valid', 'is-invalid');

  // 새 상태 클래스 추가
  if (isValid) {
    inputEl.classList.add('is-valid');
    inputEl.parentElement.classList.remove('has-error');
  } else {
    inputEl.classList.add('is-invalid');
    inputEl.parentElement.classList.add('has-error');
  }

  return isValid;
}

// HTML
// <div class="form-group">
//   <input type="text" id="username" />
//   <span class="error-msg" style="display:none">3글자 이상</span>
// </div>

const input = document.getElementById('username');
input.addEventListener('blur', () => validateInput(input));

 

마무리: classList는 선택이 아닌 필수

classList API는 DOM 요소의 클래스를 조작하는 가장 안전하고 효율적인 방식이다. className 문자열 조작은 공백 처리, 중복 체크, 부분 문자열 오류 등의 위험이 있지만, classList는 이 모든 것을 자동으로 처리해준다.

작은 최적화가 모여서 코드 품질 향상과 버그 감소로 이어진다는 점을 잊지 말자. 이 글의 "3단계 실전 예제"를 참고해 지금 프로젝트의 className 코드들을 classList로 리팩토링하면, 더 읽기 쉽고 유지보수하기 좋은 코드베이스를 얻을 수 있을 것이다.