쇼핑몰 첫 화면에 "3초 동안 배너가 서서히 나타난다"는 연출을 넣었다. 사용자가 링크를 새 탭으로 열어 두고 10분 뒤에 와 보니 배너가 여전히 반투명이다. 사내 대시보드는 로딩 점이 영영 돌고, 블로그의 "저장되었습니다" 알림은 사라지지 않는다. 오류는 하나도 없다. 셋 다 끝이 있어야 하는 일을, 멎을 수 있는 박자에 매어 둔 것이다. 에이전트에게 "애니메이션이 안 끝난다"를 맡기면 대개 연출 코드를 고친다. 연출은 멀쩡하고, 멎은 것은 박자다.
왜 숨은 탭에서 연출이 끝나지 않나
화면 다시 그리기 주기는 시계가 아니다 — 브라우저는 보이지 않는 탭에서 그 주기를 늦추거나 아예 멈춘다. 브라우저는 숨은 탭에서 requestAnimationFrame 콜백을 멈춘다(MDN: requestAnimationFrame). 자동화 브라우저나 배경 창에서는 초당 한 번이나 정확히 절반(30fps)으로 깎이기도 한다. 그러니 다음 셋은 모두 "그 박자가 계속 온다"는 가정 위에 서 있다.
- 프레임 수로 센 진행 — "프레임마다 1/180 씩 투명도를 올린다"는 180프레임이 와야 끝난다
- 잘라 낸 dt 로 센 진행 — 물리 계산이 튀지 않게 프레임 간격(dt)을 0.05초로 자르는 보정은 흔하다. 그런데 연출 타이머까지 그 dt 로 세면, 초당 1프레임인 탭에서는 1초가 0.05초로 계산돼 5초 연출이 100초가 된다
- 박자를 기다리는 부팅 — 로딩 경로에서 "다음 프레임까지 기다린다(await)"를 넣으면, 숨은 채로 새로고침된 탭은 그 프레임이 한 번도 오지 않아 로딩 화면에 영원히 머문다
한 가지가 더 있다. 다시 그리기 주기에 함께 걸린 타이머다. 화면 프레임워크에서 "그리기가 끝나고 20초 뒤 사라진다" 타이머를 그리기 코드 안에 두면, 다시 그릴 때마다 정리 단계가 타이머를 지우고, 다시 실행된 코드는 "이미 끝났음" 조건으로 빠져나가 타이머가 영영 걸리지 않는다. 박자가 멎은 게 아니라 박자가 너무 자주 와서 끝이 지워진 경우다.

어떻게 고치나 — 끝은 벽시계로, 기다림엔 상한을
끝이 있어야 하는 일은 "몇 번 불렸나"가 아니라 "시작한 뒤 몇 초 지났나"로 진행을 잰다. 다시 그리기 콜백은 그리는 일만 하고, 지금 어디까지 왔는지는 매번 벽시계에서 새로 계산한다. 그러면 탭이 10분 숨었다가 돌아와도 첫 프레임에 바로 끝 상태가 그려진다.
순서는 이렇다.
- 박자에 매인 일을 둘로 가른다. 물리·보간처럼 "튀면 안 되는" 계산과, 연출·페이드·부팅처럼 "끝나야 하는" 일. dt 를 자르는 보정은 앞쪽에만 쓴다
- 끝나야 하는 일은 시작 시각을 저장하고 진행률을
(지금 − 시작) ÷ 길이로 계산한다. 프레임마다 더하는 누적값을 쓰지 않는다 - 부팅 경로의 모든 기다림에 제한 시간을 둔다. 다음 프레임 기다리기, 셰이더 컴파일 같은 비동기 작업을
setTimeout과 경주시켜(Promise.race) 몇 초 안에 안 오면 다음 단계로 넘어간다. 숨은 탭에서도setTimeout은 늦게나마 불린다(초 단위로 늦춰질 뿐이다 — MDN: setTimeout) - "N초 뒤" 타이머는 그리기가 아니라 그 일의 수명에 단다. 다시 그릴 때마다 정리되는 자리에 두지 말고, 화면이 사라질 때 한 번만 정리되는 자리에 둔다
- 숨은 탭으로 직접 확인한다. 개발자 도구가 열린 탭은 보이는 것으로 취급돼 버그가 재현되지 않는다. 도구를 닫고, 창을 뒤로 보낸 채 새로고침하고, 돌아와서 본다. 탭이 보이는지는 Page Visibility API 의
document.visibilityState로 찍는다
에이전트 지침에 넣을 문장
연출·로딩·알림이 "안 끝난다"는 보고를 받으면 연출 코드보다 박자를 먼저 의심하라.
1) 그 일이 프레임 수·잘라 낸 dt·"다음 프레임 기다리기"로 진행되는지 찾는다.
2) 끝나야 하는 일은 시작 시각을 저장하고 (지금 − 시작) ÷ 길이로 진행률을 계산하게 바꾼다.
3) 부팅 경로의 모든 기다림에 setTimeout 경주로 제한 시간을 둔다.
4) "N초 뒤" 타이머가 다시 그리기 때마다 정리되는 자리에 있으면 그 일의 수명 쪽으로 옮긴다.
5) 개발자 도구를 닫고 탭을 숨긴 채 새로고침해서 재현·확인한다.
우리가 겪은 일
서로 다른 두 그래픽 프로젝트에서 났다.
- 5초 연출이 100초 — 붓으로 그려 나타나는 연출이 숨은 탭에서 끝나지 않았다. 물리용으로 0.05초에 자른 dt 를 연출 타이머도 쓰고 있었고, 초당 1프레임인 탭에서 진행이 20배 느려졌다
- 분홍 점에서 멈춘 로딩 — 로딩 경로가 다음 프레임을 기다리고 있어, 다른 창을 보는 동안 새로고침한 탭이 영원히 로딩 화면이었다. 개발자 도구를 연 상태에서는 재현되지 않았다. 같은 자리의 셰이더 컴파일 기다림에도 제한 시간을 두었다
- 사라지지 않는 붓글씨 — "다 쓰고 20초 뒤 사라짐" 타이머를 그리기 효과 안에 두었더니, 다시 그릴 때마다 정리가 타이머를 지우고 재실행은 "이미 끝남"으로 빠져나가 타이머가 걸리지 않았다
- 자동화 브라우저의 빈 화면 — 같은 이유로 자동화·배경 탭의 3D 화면이 비고 fps 가 딱 30 이나 1 안팎으로 찍혀, 코드가 깨진 것처럼 보였다