로딩 스피너 깜빡임의 두 끝 — 앞쪽은 CSS, 뒤쪽은 JS

로딩 스피너의 깜빡임은 양 끝에서 따로 생기는데 CSS는 한쪽만 잡는다.

언제: 스피너가 한 프레임 번쩍하고 사라지거나, 떴다가 하드컷으로 끊길 때. 앞쪽인지 뒤쪽인지부터 판별한다.

증상 어느 쪽 처방
응답이 너무 빨라 스피너가 뜨기 전이나 직후 사라진다 앞쪽(leading) CSS — animation-delay/transition-delay
스피너가 떴는데 너무 짧게 보이고 하드컷된다 뒤쪽(trailing) JS — useMinimumLoading

절차

  1. 앞쪽이면 지연을 준다. 응답이 지연보다 빨리 오면 한 프레임도 안 보인다 — 스피너를 통째로 건너뛴다.
  2. 뒤쪽이면 unmount를 max(응답 시각, 최소 노출 시간)으로 민다. 한 번 뜬 스피너가 최소 노출 시간만큼은 보이게 된다.

확인: 응답을 지연보다 빠르게 만들어 본다 — 스피너가 한 프레임도 안 뜨면 앞쪽이 잡힌 것이다. 응답을 최소 노출 시간보다 빠르게 만들어 본다 — 스피너가 그 시간만큼 유지되면 뒤쪽이 잡힌 것이다.

왜 갈리나: CSS는 시작만 통제한다. 스피너가 보이는 구간은 [뜬 시각, unmount 시각]인데, 지연은 앞쪽을 미루고 지속 시간은 애니메이션 길이일 뿐 요소가 DOM에 남는 시간이 아니다. 뒤쪽 끝은 응답이 언제 오느냐가 정하고 그건 CSS가 모르는 값이다. 그래서 “최소 몇 ms는 보인다”를 CSS만으로는 보장할 수 없다.

함정

  • 억제력은 지연에서만 나온다 — @starting-style 단독(지연 0)으론 앞쪽을 못 막는다.
  • 이미 떴다는 사실은 못 되돌린다. 뒤쪽은 CSS가 가진 두 값(지연·지속 시간)으로 잡을 수 없고 사라지는 시각 자체를 미뤄야 한다 — CSS 밖, JS 타이밍이다.

곁가지 — @starting-style같은 뿌리의 반대편이다. transition은 두 상태 사이를 보간하는데, 진입에는 옮겨갈 이전 상태가 없고1 퇴장에는 요소 자체가 이미 사라진다. @starting-style은 앞쪽의 출발값을 대신 정해주고(진입 fade-in), useMinimumLoading은 뒤쪽의 unmount를 미룬다. 뿌리는 같아도 손대는 지점이 달라 별개 결정이다.

Footnotes

  1. CSS transition은 요소의 첫 스타일 적용이나 display: none → 표시 전환에서는 기본적으로 발동하지 않는다. @starting-style이 “무엇에서 출발할지”를 정의해 그걸 가능하게 한다. (@starting-style — MDN)

#573raw