이번주에 버그로 등록된 이슈 하나를 해결했던 과정이 재미있어서 기록으로 남겨두는 게 좋겠다는 생각이 들었다. 먼저 제보된 이슈는 다음과 같았다.
특정 문서를 업로드했을 때 편집 화면에서 스크롤 이동이 되지 않고, A 기능이 동작하지 않음
A 기능은 편집 화면 내에서 스크롤 이동시 뷰포트에 들어와 있는 아이템을 대상으로 서버에 배치로 요청을 한 다음, 해당 응답 결과를 가지고 화면에 특정 UI로 렌더링하는 기능이었기 때문에 이건 스크롤 이슈였다.
먼저 해당 현상을 재연하기 위해 버그가 발생한다고 제보한 계정으로 접속해 해당 문서를 확인해 보았다. 그런데 내 컴퓨터에서는 스크롤도, A 기능도 정상적으로 동작했다. 내 컴퓨터는 맥북이었고, 버그를 제보한 분의 컴퓨터는 윈도우 데스크탑이었다. 그래서 팀 공용 윈도우 노트북으로 확인해 보았다. 마찬가지로 정상 동작했다. 회사의 다른 윈도우 데스크탑 컴퓨터에서도 정상 동작했다. 이때부터 혼란이 시작되었다 ㅋㅋㅋㅋ 버그를 재연하지 못하면 디버깅을 할 수가 없잖아 😇 일단 버그 리포트에 위의 세 가지 환경에서 재연이 되지 않는다고 코멘트를 작성했다.
그러자 얼마 후 버그를 제보한 분이 본인 컴퓨터에서 상황을 재연해서 보여주겠다고 했다. 같이 살펴보니 그 컴퓨터에서도 항상 같은 현상이 발생하는 건 아니었다. 브라우저 사이즈가 최대화가 아닐 때는 정상 동작했고, 최대화 버튼을 눌러서 브라우저 크기를 화면 크기에 꽉 차게 할 때만 버그가 발생했다. F11을 눌러 전체 화면 모드로 바꾸면 버그가 사라졌다. 브라우저 사이즈를 작게 해둔 상태에서 먼저 편집 화면에 진입한 다음 브라우저 사이즈를 최대화로 바꿀 때에도 정상 동작했다. 모니터 해상도와 작업 표시줄의 위치나 설정 등을 바꿔보니 최대화 사이즈에서도 정상 동작했다. 편집 화면에서 아이템의 height는 동적으로 변경되기 때문에 브라우저 사이즈나 모니터 해상도, 작업 표시줄 등의 설정이 바뀌면 뷰포트에 들어오는 아이템의 개수가 바뀐다. 즉, 아이템 리스트를 보여주는 영역에서 동적인 아이템 개수로 인해 뷰포트 내에서 특정 크기의 영역을 차지할 때 발생하는 이슈였다.
개발자 도구를 열어 요소 검사를 해보니 역시나 뷰포트 내에 있는 아이템들을 감싸고 있는 wrapper 요소가 영역을 이상하게 차지하고 있었다. 리스트 영역에서 무한 스크롤을 구현하기 위해 tanstack virtual을 사용 중이었는데 개발자 도구에서 확인해 보니 0번째 아이템부터 뷰포트 내에 있는 첫번째 아이템까지의 height를 더한 값만큼 transform: translateY()를 계산하는 컨테이너 요소의 style 값이 스크롤 이동을 전혀 하지 않아서 뷰포트 내에 있는 아이템 개수가 일정함에도 불구하고 쉬지 않고 translateY 값을 재계산하고 있었다.
tanstack virtual은 뷰포트 내에 있는 아이템만 DOM 트리에 추가하고 뷰포트를 벗어나면 DOM 트리에서 아이템을 제거하는 방식으로 최적화를 할 수 있는 라이브러리인데 사용자가 스크롤 이동을 하지 않고 있음에도 뷰포트 내에 있다고 판단하는 첫번째 아이템이 계속 DOM 트리에 들락날락 거리고 있었고, 이에 따라 translateY 값이 계속 바뀌면서 재계산을 하고 있어서 스크롤이 먹히지 않는 거였다. 이 상황에서 개발자 도구의 요소 검사에서 DOM 트리의 아이템인 <li> 요소 1개를 삭제하거나 복제해보면 다시 정상 동작했다. 리스트 영역 크기가 바뀌면 버그가 재연되지 않는다는 가설에 다시 한 번 힘을 실을 수 있었다.
이제 이러한 이슈가 발생하는 원인에 대한 가설을 세우고 하나씩 검증을 해봐야 할 시간이었다. 위에서 언급했다시피 리스트 영역의 컨테이너 wrapper에서는 tastack virtual을 사용하고 있었고, 개별 아이템은 드래그앤드롭으로 순서를 바꿀 수 있어야 해서 react-beautiful-dnd를 사용하고 있었다. 특정 환경에서 두 라이브러리 간의 충돌이 있는 걸까 싶어서 먼저 dnd 라이브러리 관련 코드를 제거해보았다. 동일한 버그가 발생했다. 다행히도 라이브러리 충돌 문제는 아녔다. 휴 😅
리스트 영역 내의 요소를 살펴보니 전체 컨테이너 <div> 요소 바로 아래에 있는 <ul> 요소가 뷰포트 내에 있는 아이템들의 wrapper 역할을 하고 있고, 그 안에 <li> 요소가 아이템의 wrapper 역할을 하고 있었다. 그런데 <li> 요소 중간중간에 구분선 역할을 하는 <div> 요소가 있었다. 혹시 구분선 역할을 하는 <div>의 height 값인 5px이 tanstack virtual이 height 계산을 하는데 방해를 하고 있는건 아닐까 싶어 구분선을 전부 제거해 보았다. Gotcha, 버그가 사라졌다!
코드를 다시 찬찬히 살펴보니 동적인 아이템 크기를 측정하기 위해 ref로 전달하는 virtualizer.measureElement가 <li> 요소만을 대상으로 하고 있었고, 구분선 <div> 요소는 포함하지 않고 있었다.
ref={virtualizer.measureElement}
원인을 파악하고 나니 해결은 금방이었다. virtualizer.measureElement로 전달된 ref가 <li> 아이템과 <div> 구분선을 모두 포함할 수 있게 마크업 구조를 수정하니 이걸로 끝이었다.
드래그앤드롭으로 아이템 순서 이동이라는 추가 기능 구현을 위해 dnd 라이브러리의 Draggable 컴포넌트를 붙이면서 아이템의 마크업 구조를 변경했는데, 이때 버츄얼에서 계산에 필요한 ref가 전달되는 요소를 면밀히 살피지 못한 탓에 발생한 버그였다. 하지만 운이 좋다고 해야 할지, 나쁘다고 해야 할지(?) 특정 조건을 만족할 때가 아니면 스크롤 이동이 정상 동작했기에 3개월이 넘도록 이러한 이슈가 있다는 것을 인지하지 못했다.
그렇다면 왜 특정 조건 하에서만 이렇게 translateY 값을 재계산하는 걸까? 이 질문에 대한 답은 찾지 못했다. 더 정확히는 이 질문에 대한 답을 찾기 위해서 어떻게 해야 하는지 알지 못한다.. 어떤 키워드로 검색을 해야 하는 걸까? tanstack virtual 레포의 이슈나 디스커션을 뒤져야 하는걸까?
개인적으로 디버깅에 대한 배휘동 님의 인프콘 세션을 무척이나 기대하고 있는데, 이번주에 인프콘에서 발표하실 내용의 도입부를 미리 볼 수 있는 포스팅이 올라왔다.
실제로 이번 이슈를 해결하기까지 원인을 파악하는 데에 전체 디버깅 시간의 절반 가량을 썼고, 정확한 원인을 파악하자 문제 해결은 금방할 수 있었다. 원인을 파악하기까지 했던 과정을 다시 복기해 보면 다음과 같이 정리할 수 있을 것 같다.
1. 동일한 현상 재연
2. 이슈가 발생하는 원인에 대한 가설 세우기
3. 해당 가설에 대한 검증
4. 2-3의 과정을 반복
5. 원인 파악
6. 해결
1~5번의 과정에서 동료 분의 컴퓨터에서만 재연이 가능했기에 본의 아니게(?) 디버깅 과정을 함께할 수 있었다. 가설을 세우고 검증해 나갈 때, 포스팅에 모든 대화들을 상세히 쓰지는 않았지만 각자가 생각하는 가설을 하나씩 얘기하며 답을 찾아가는 시간이 꽤나 즐거웠다. 함께 하는 디버깅의 즐거움을 처음 맛본 경험이었기에 기록해두고 싶었다. 이런 찰나의 기쁨을 소중히 여겨야지.
📚 추가로 보면 좋은 포스팅
'돌멩이 하나 > 에러는 미래의 연봉' 카테고리의 다른 글
next.js로 만든 토이 프로젝트 배포 (0) | 2024.09.01 |
---|---|
custom event로 렌더링과 무관한 데이터 추적하기 (0) | 2024.08.11 |
Checkbox 컴포넌트를 만들면서 알게 된 것들 (0) | 2024.05.22 |
React Query를 활용한 테이블에서 데이터 정렬하기 (0) | 2024.03.21 |
React ErrorBoundary와 react-query를 사용하여 예외 처리 설계하기 (0) | 2024.03.10 |