막바지에 다다른 FPS Aim Test의 마무리 작업으로,
최근에는 새로운 기능 추가보다는 주석 등 컨벤션 점검과
기존 기능을 안정화하고, 예상치 못한 버그를 해결하는 작업이 많았다.특히 resize, fullscreen 전환, Canvas 렌더링과 관련된 문제들은
각각 독립적인 이슈처럼 보였지만,
실제로는 하나의 문제에서 파생된 연쇄적인 트러블에 가까웠다.기억이 비교적 선명할 때,
최근에 겪었던 트러블슈팅 과정을 순서대로 정리해 보려고 한다.
해상도 변경이나 모드 전환에서는 크게 티가 나지 않았지만,
마우스로 뷰포트를 직접 드래그해 resize할 때는
문제가 비교적 명확하게 드러났다.
원인은 resize 시점에 실행되던 dpr(devicePixelRatio) 조정 로직이었다.
이 로직은 브라우저 해상도와 Canvas 해상도를 픽셀 단위로 일치시켜
화면 품질을 유지하는 역할을 한다.
화면이 변할 때마다 즉각적으로 실행되어야 한다고 판단해 구현했었는데,
문제는 연산의 무거움이 아니라,
너무 자주 실행되고 있다는 점이었다.
결과적으로 점멸 현상은 크게 줄었지만,
뷰포트를 빠르게 드래그할 경우
맵과 타겟 컨테이너의 비율 조정이 한 타이밍 늦게 반응하는 경우가 생겼다.
하지만 눈에 띄는 점멸보다
간헐적인 지연이 UX 리스크가 더 낮다고 판단해
현재 상태를 유지하기로 했다.
성능 문제는 항상 “무거운 연산”에서만 발생하지 않는다.
빈도가 높은 가벼운 연산도 충분히 UX를 망칠 수 있다.
육안으로도 “한 번 튄다”는 느낌이 확실히 보였다.
원인을 추적해보니, 모드 변경 직후 아주 짧은 타이밍 동안
drawSize가 실제 canvas 크기보다 작은 값으로 계산되는 구간이 존재했다.
이 상태에서
정확히는 drawSize가 0에 가까운 값으로 계산되는 첫 프레임에
클램프 결과가 잘못 확정되고 있었던 것이다.
계산식 자체는 맞았지만,
계산이 실행되는 타이밍이 문제였다.
drawSize가 canvas 크기보다 작을 경우렌더링 문제는 로직 문제만이 아니라
타이밍 문제로도 충분히 발생할 수 있다는 점을 다시 체감했다.
그동안 애니메이션은
미리 계산된 결과를 따라 움직이는 것이라 생각했지만,
실제로는 매 프레임 계산을 통해 결과에 도달한다는 점을
이번 경험을 통해 명확히 이해하게 되었다.
매번 발생하는 문제는 아니었고, 대략 재현 시도 10번 중 1~2번,
아주 빠른 모드 변경을 반복할 때 드물게 발생했다.
못 알아챘다면 모를까 한 번 인지한 이상,
그대로 두고 넘어갈 수는 없는 문제였다.
문제의 핵심은
Canvas의 실제 픽셀 크기 변경과
React 리렌더 타이밍 사이에
명확한 선후관계가 보장되지 않는다는 점이었다.
Canvas 크기는 이미 변경되었지만,
앞서 살펴본 카메라 클램프 문제와 유사했다.
즉, 값은 바뀌었지만
그 값을 기준으로 다시 계산하는 과정에 빈틈이 있는 상태였다.
Canvas처럼 React 외부에서 상태가 변하는 요소는
React 상태 변경과 자동으로 동기화될 것이라 기대하면 안 된다.
특히 useRef를 의존성 배열로 사용할 때는
참조값과 원시값을 명확히 구분해야 한다는 점을 다시 인식했다.
useFullscreen, useResize, useBorderFade 의 내부 로직이UX 상 눈에 띄는 오류는 없었지만,
의미 없는 반복 재생성 자체가
성능을 갉아먹고 있다는 점은 분명했다.
원인은 크게 두 가지였다.
useMemo 없이 반환되고 있어훅 내부 로직이 문제가 아니라,
훅의 props와 반환값의 참조 안정성이 깨진 상태였다.
useMemo 적용훅 최적화에서는
로직의 안정성뿐 아니라
입력값과 반환값의 참조 안정성도 함께 고려해야 한다는 점을 실감했다.
이번 트러블슈팅들을 겪으며 느낀 점은 비교적 명확하다.
기능 추가보다
이런 안정화 작업이
체감 난이도는 훨씬 높게 느껴진다는 점이다.
resize, fullscreen, canvas처럼
브라우저 환경과 밀접한 요소들은
한 번 고쳤다고 끝나는 문제가 아니었다.
앞으로 비슷한 구조의 프로젝트를 다시 만든다면,
를 훨씬 더 먼저 고려하게 될 것 같다.