마커 드래그 Maximum update depth exceeded 트러블슈팅
배경
도면 마커 기능 UX를 개선하면서 저장 방식을 바꿨다.
- 마커/결함그룹 위치:
dragEnd자동 저장 - 마커 생성: 즉시 저장
- 마커 번호/색상: 즉시 저장
- 결함 상세: 버튼 저장 유지
기능 자체는 맞았지만, 결함이 2개 이상 묶인 마커를 드래그할 때, 아래 에러가 발생했다.
Maximum update depth exceeded
처음 내가 든 생각과 오해
1) "좌표 setState 때문에 터진다"
틀린말은 아니지만 정확히는 setState 자체가 문제라기보다, 고빈도 mousemove + 다른 상태 동기화/effect 반응이 겹치면서 업데이트 연쇄(depth)가 생긴 게 핵심이었다.
2) "mousemove 업데이트를 빼고 dragEnd만 처리하면 된다"
이렇게 하면 드래그 중 위치가 따라오지 않거나 점프한다.
정답은 역할 분리였다.
- UI 반영:
mousemove - 서버 저장(API):
dragEnd
3) "depth exceeded면 JS 콜스택 오버플로우다"
브라우저 call stack 문제가 아니라, React가 렌더 루프(렌더 -> setState -> 렌더 반복)를 감지하고 중단한 것이다. (React 보호장치 느낌)
rAF를 넣고 정리된 기준
처음에는 requestAnimationFrame이 추상적으로 느껴졌는데, 구현하면서 느낀 건 다음과 같았다.
mousemove는 한 프레임 (60Hz라면 1초에 60번 화면 갱신) 안에서도 여러 번 들어온다- 이벤트마다 바로
setState하지 않고, 최신 좌표만 pending으로 덮어쓴다 - 화면 갱신 직전에 1번만 반영한다
핵심은 "다음 드래그 전에 1번"이 아니라, "다음 화면 갱신 타이밍 전에 1번" 이다.
내가 최종적으로 적용한 안정화 포인트 3가지
mousemove를 즉시setState하지 않고rAF로 배치- 같은/유사 좌표는 업데이트 스킵
dragEnd시점에 늦게 들어올 수 있는 stalerAF콜백 정리
추가로, 자동 저장 로직을 수정할 때는 기능 체크를 같이 해야 한다.
실제로 중간에 병합 분기가 빠져서 병합 기능이 한 번 깨졌었다.
이번에 정리된 나의 생각
- 드래그는 "상태를 많이 바꾸는 문제"보다 상태 변경 빈도 제어 문제에 가깝다
Maximum update depth exceeded는 "콜스택"이 아니라 업데이트 루프 감지로 읽어야 한다- UI 반영(
mousemove)과 서버 저장(dragEnd)은 분리해야 한다 - 간헐적인 버그일수록 프레임 타이밍, stale 콜백, race를 먼저 본다