로딩 스피너 깜빡임의 두 끝 — 앞쪽은 CSS, 뒤쪽은 JS
스피너가 한 프레임 번쩍하고 사라지거나, 떴다가 하드컷으로 끊긴다. 깜빡임은 양 끝에서 따로 생기는데 CSS는 한쪽만 잡는다. 어느 끝인지는 응답 시각을 붙잡고 보인 시간을 재서 가른다 — DOM에 붙어 있던 시간이 아니다.
흐름
- headless Chrome + Node 전역
WebSocket으로 raw CDP. MCPemulate는 프리셋 스로틀링뿐이라 요청 하나를 못 붙잡는다. - 프로브 — 스피너가 사라질 때 보인 시간을 돌려준다.
- 경계값이면
disabled-by-default-devtools.screenshottrace로 확인한다. 스크린샷은 바뀐 프레임에만 찍히니 처음 물든 장 → 다음 흰 장이 보인 시간이다. - 응답 < 지연인데 보였으면 앞쪽, 응답 > 지연인데 최소 노출보다 짧으면 뒤쪽.
| 지연 | 응답 | 최소 노출 | 프로브 | trace | 판정 |
|---|---|---|---|---|---|
| 0 | 100 | — | 9프레임 · 133ms | 앞쪽 | |
| 300 | 100 | — | 0 | 물든 장 없음 | 앞쪽 억제됨 |
| 300 | 350 | — | 4프레임 · 66ms | 약 48ms, 불투명도 최대 약 0.8 | 뒤쪽 |
| 300 | 350 | 500 | 515ms | 뒤쪽 잡힘 | |
| 300 | 100 | 500 | 0 | 앞쪽 억제 유지 |
실측 조건은 각주에 있다.1 셋째 줄이 뒤쪽 깜빡임의 실제 모습이다 — 페이드가 끝나기 전에 끊겨 한 번도 제대로 진해지지 못하고 사라진다.
처방
- 앞쪽 —
animation-delay/transition-delay. 응답이 지연보다 빠르면 스피너를 통째로 건너뛴다.@starting-style단독(지연 0)으로는 못 막는다. - 뒤쪽 — CSS는 시작만 통제하고 끝은 CSS가 모르는 응답 시각이 정하니, 사라지는 시각을 JS에서 민다(
useMinimumLoading). 최소 노출은 처음 보인 시각부터 잰다: 사라지는 시각 =max(응답 시각, firstVisibleAt + 최소 노출). 요청 시점부터 재면 표 마지막 줄에서 안 떠도 될 스피너를 붙잡는다.
밟은 것
spawn한 프로세스만 죽이면 Chrome 자식이 남아 다음 실행이 옛 페이지에 붙는다 →detached: true+process.kill(-pid).- 옛 페이지에 붙은 채
Runtime.enable하면 이전 console 메시지가 다시 온다. - 포트가 뜨기 전
/json은ECONNREFUSED→ 폴링. - 첫 측정은 느리다(hold 100에 응답 149ms) → 첫 줄은 다시 잰다.
곁가지 — @starting-style은 같은 뿌리의 반대편이다. transition은 두 상태 사이를 보간하는데, 진입에는 옮겨갈 이전 상태가 없고2 퇴장에는 요소 자체가 이미 사라진다. @starting-style은 앞쪽의 출발값을 대신 정해주고(진입 fade-in), useMinimumLoading은 뒤쪽의 unmount를 미룬다. 뿌리는 같아도 손대는 지점이 달라 별개 결정이다.
Footnotes
-
Chrome 154 headless 에서 raw CDP 로 실측 (2026-09-30).
/api요청을Fetch.requestPaused로 100·350ms 붙잡고, 스피너(animation: fade 100ms <지연> both)의opacity를 매requestAnimationFrame마다 읽었다. 프로브를 먼저 걸고 버튼 클릭으로 스피너를 띄운 실행에서도 hold 100 → 0프레임, hold 350 → 2프레임(34ms)으로 같았다. 프로브 대 trace 비교는 스피너를 400px 빨간 상자로 두고Tracing의Screenshot이벤트를 받아, JPEG 를sips로 BMP 로 바꿔 상자 안 한 점의 픽셀을 읽었다 — hold 350 에서(255,199,200)→(255,109,110)→(255,51,52)→ 흰색. ↩ -
CSS transition은 요소의 첫 스타일 적용이나
display: none→ 표시 전환에서는 기본적으로 발동하지 않는다.@starting-style이 “무엇에서 출발할지”를 정의해 그걸 가능하게 한다. (@starting-style — MDN) ↩