대니
danny's blog@dannywon_dev
발행 129 · 대기 183

1인 개발자. 클로드 코드를 팀처럼 굴린다. 매일 겪은 것만 쓴다.

버그와 함정

requestAnimationFrame 이 안 도는데 에러도 안 났다 — visibilityState 가 hidden

Claude in Chrome 으로 자동 스크롤을 검증하다 rAF 가 0번 실행된 걸 발견했다. 원인은 hidden 이었다.

드래그로 그리드를 늘리다 커서가 화면 끝에 닿으면 자동으로 스크롤되는 동작을 만들다 보면, 코드는 맞는데 스크롤이 1px 도 안 움직이는 상황을 만난다. 콘솔에 에러도 없다.

nodnod(전시·아트페어 관람 앱) admin 콘솔의 플로어맵 에디터에 리사이즈 핸들을 붙이는 작업이었다. 우하단 모서리를 드래그해서 그리드 칸 수를 정하고, 커서가 캔버스 끝에 닿으면 자동으로 스크롤되면서 그리드가 계속 커진다. 피그마나 구글 슬라이드에서 흔히 보는 동작이다.

구현은 requestAnimationFrame 으로 했다. 매 프레임 커서 위치를 검사해서 가장자리 근처면 scrollLeft/scrollTop 을 늘리고, 그 위치 기준으로 그리드 크기를 다시 계산하는 루프다. 표준적이고 부드러운 선택이라고 생각했다.

scrollLeft 가 정확히 0에서 안 움직였다

Claude in Chrome 으로 실제 동작을 검증했다. 마우스를 화면 가장자리로 옮긴 뒤 몇 초를 기다려도 container.scrollLeft 가 0 그대로였다. 에러는 없었다. 로직을 몇 번 다시 읽어도 틀린 데가 안 보였다.

그래서 환경을 찍어봤다.

  • document.hasFocus()true
  • document.visibilityStatehidden

window.requestAnimationFrame 을 감싸서 호출 횟수를 세는 코드를 심었다. mousedown 시점에 최초 1회 호출된 뒤, 5초 동안 정확히 0번 더 실행됐다. 루프가 첫 프레임에서 끊긴 것이다.

「화면이 그려진다」와 「사람이 보고 있다」가 다른 기준이었다

화면이 안 보이면 rAF 가 멈춘다는 건 알고 있었다. 다만 그 조건을 "탭이 실제로 화면에 안 그려질 때"로만 생각하고 있었다.

이 자동화 도구는 스크린샷이 매번 정상으로 캡처됐다. 즉 화면은 그려지고 있었다. 그런데도 visibilityState 는 hidden 으로 잡혔다. 브라우저가 "이 탭을 사람이 보고 있다"고 판단하는 기준과 "화면을 합성해서 캡처할 수 있다"는 기준이 같지 않았다. 내가 틀렸던 건 코드가 아니라 이 전제였다.

setInterval 은 스로틀됐지만 멈추진 않았다

같은 조건에서 setInterval(fn, 16) 으로 바꿔 테스트했다. 완전히 안 멈추진 않았고 대략 500ms 간격으로 스로틀됐지만, 최소한 계속 실행은 됐다. 실제 코드를 setInterval 기반으로 다시 짜서 재검증하니 자동 스크롤과 그리드 리사이즈가 정상 작동했다.

실사용자 브라우저(포그라운드 탭)에서는 애초에 이 문제가 안 났을 것이다. 마우스를 드래그하려면 그 탭을 보고 있어야 하니까. 그래서 이건 사용자가 만날 버그가 아니라 검증 환경에서만 드러난 버그다.

다만 그게 rAF 를 그대로 둘 이유는 못 됐다. 자동화나 백그라운드 실행 가능성이 조금이라도 있는 UI 인터랙션이라면, rAF 하나만 믿기보다 완전히 멈추지는 않는 대안을 같이 두는 쪽으로 바꿨다.

에러가 없다는 건 코드가 맞다는 뜻이 아니었다. 콜백이 아예 안 불린 거라 던질 에러도 없었을 뿐이다. 호출 횟수를 세보기 전까지 그걸 몰랐다.

← 목록으로