tech

마커 드래그 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가지

  1. mousemove를 즉시 setState하지 않고 rAF로 배치
  2. 같은/유사 좌표는 업데이트 스킵
  3. dragEnd 시점에 늦게 들어올 수 있는 stale rAF 콜백 정리

추가로, 자동 저장 로직을 수정할 때는 기능 체크를 같이 해야 한다.
실제로 중간에 병합 분기가 빠져서 병합 기능이 한 번 깨졌었다.


이번에 정리된 나의 생각

  • 드래그는 "상태를 많이 바꾸는 문제"보다 상태 변경 빈도 제어 문제에 가깝다
  • Maximum update depth exceeded는 "콜스택"이 아니라 업데이트 루프 감지로 읽어야 한다
  • UI 반영(mousemove)과 서버 저장(dragEnd)은 분리해야 한다
  • 간헐적인 버그일수록 프레임 타이밍, stale 콜백, race를 먼저 본다