버튼 자체는 크기를 유지한 채(`min-width`), 내부 콘텐츠만 라벨 → 스피너 → 체크 순으로 `opacity`를 교차시킨다. 버튼 크기가 상태마다 바뀌면 주변 레이아웃이 들썩여 산만하므로, 폭은 가장 긴 상태(라벨)에 맞춰 고정한다.
3가지 상태(대기·로딩·완료)를 클래스(`.loading`, `.done`)로 나타내고, 실제 비동기 요청과 최소 로딩 시간(예: 400ms 이상)을 함께 지켜야 한다 — 응답이 너무 빨리 오면 스피너가 깜빡였다 사라져 오히려 불안정해 보인다.
로딩 중에는 반드시 버튼을 비활성화(`disabled` 또는 클릭 무시)해서 중복 제출을 막는다. 실패 케이스도 마련해야 한다 — 보통 에러 상태(빨간 테두리·흔들림)로 짧게 보여준 뒤 원래 상태로 돌아온다.
언제 쓰나
폼 제출, 결제, API 호출처럼 지연이 있는 모든 버튼 액션에 기본으로 둡니다. 즉시 반응하는 로컬 토글에는 필요 없습니다.