버그를 마주했을 때 우리가 가장 먼저 하는 일은 코드를 들여다보는 것이다. 스택 트레이스를 훑고, 콘솔 로그를 확인하고, 같은 흐름을 다시 실행해본다. 그런데 이상하게도 내 화면에서는 멀쩡하고, 특정 사용자에게만, 특정 조건에서만 터진다. 이 순간 대부분의 개발자는 '재현이 안 되는 버그'라는 카테고리에 집어넣고 우선순위를 낮춘다. 하지만 최근 눈에 띈 세 가지 사례는 그 판단이 얼마나 위험한지를 조용히 증명한다.
첫 번째 사례는 Modal 안에서 Drag & Drop을 구현하다 마주친 위치 튕김 현상이다. velog에 공유된 COTATO 프로젝트 분석 글에 따르면, react-beautiful-dnd로 구현한 DnD가 다른 페이지에서는 정상 동작하는데 유독 모달 위에서만 카드가 엉뚱한 위치로 튀어나갔다. 코드는 동일하다. 라이브러리도 동일하다. 달라진 건 단 하나, 모달이라는 컨텍스트였다.
원인은 CSS 스펙 깊숙한 곳에 있었다. position: fixed 요소의 컨테이닝 블록은 기본적으로 뷰포트지만, 조상 요소에 transform 속성이 none이 아닌 값으로 적용돼 있으면 그 조상이 컨테이닝 블록으로 바뀐다. MUI Modal은 내부적으로 transform: translate(-50%, -50%)로 모달 박스를 중앙에 배치한다. 결과적으로 드래그 시 position: fixed로 전환된 카드는 뷰포트가 아닌 모달을 기준으로 top/left를 계산했고, getBoundingClientRect()가 반환한 뷰포트 기준 좌표와의 불일치가 위치 튕김으로 나타났다. 해결책은 두 가지였다. 모달의 중앙 정렬 방식을 transform 대신 flex로 교체해 컨테이닝 블록 문제를 원천 차단하거나, 드래그 중인 요소에 createPortal을 적용해 DOM 트리상 모달 외부에 렌더링함으로써 모달의 CSS 영향권에서 벗어나는 것이다. 핵심은 버그의 원인이 DnD 로직이 아니라 모달이 만들어낸 CSS 컨텍스트에 있었다는 점이다.
두 번째 사례는 더 흥미롭다. 웹 게임에서 울타리 방향이 한쪽으로만 그려진다는 신고가 반복됐다. 개발자의 기기에서는 재현되지 않았고, 아이패드 프로에서도 정상이었다. 아이패드 에어에서는 재현됐다. 자연스러운 가설은 '특정 기기 문제'였다. 그런데 velog에 공유된 이 디버깅 회고가 인상적인 건, 개발자가 그 가설을 검증하기 위해 게임 내에 신고 버튼을 직접 심었다는 점이다. 버튼을 누르면 클릭 좌표, 화면 줌 비율, 기기 정보, 브라우저 정보가 그대로 서버로 전송됐다.
로그가 쌓이자 패턴이 보였다. 기기가 아니었다. 기기와 무관하게 화면을 확대(줌)한 사용자에게서만 버그가 발생했다. 줌이 적용된 상태에서는 터치 좌표와 실제 게임 격자의 좌표가 미세하게 어긋났고, 그 차이가 엉뚱한 방향의 울타리로 이어졌다. '아이패드 에어 사용자가 재현했던 이유'는 그 기기를 쓰는 사람들이 우연히 화면을 확대해서 쓰고 있었던 것뿐이었다. 기기 탓이 아니라 사용 습관 탓이었다. 코드를 아무리 들여다봐도 보이지 않았던 버그가, 사용자의 실제 화면 상태 데이터를 수집하는 순간 해결의 실마리를 드러냈다.
세 번째 사례는 조금 다른 층위의 이야기지만 맥락은 같다. dev.to에 공개된 스케줄링 도구 설계 분석에 따르면, 대부분의 캘린더 예약 도구는 '슬롯 그리드' 방식으로 작동한다. 가용 시간을 일정 간격으로 미리 나눠두고, 기존 일정과 겹치는 슬롯을 차단하는 식이다. 문제는 이 방식이 실제로 존재하는 예약 가능 시간을 숨긴다는 점이다. 예를 들어 9시 45분에 끝나는 회의가 있다면, 9시와 9시 30분 슬롯은 차단되지만 9시 45분부터 10시 15분까지의 30분 창은 완전히 열려 있다. 그러나 그리드 기반 도구는 이 시간을 보여주지 않는다.
대안으로 제시된 '프리 윈도우(free window)' 방식은 가용 시간을 연속 구간으로 두고, 여기서 바쁜 시간들을 빼나가는 방식이다. 남은 구간 안에 원하는 예약 시간이 들어올 수 있다면 어디서든 예약이 가능하다. 9시 45분, 10시, 10시 15분 모두 유효한 시작점이 된다. 이 방식은 특히 하루가 꽉 찬 사람일수록, 즉 스케줄링 도구가 가장 필요한 사람일수록 숨겨진 가용 슬롯을 정확히 드러내준다. 사용자가 실제로 예약 가능한 시간이 있음에도 '꽉 찼다'는 인상을 받는 UX 버그는 코드 오류가 아니라 잘못된 알고리즘 가정에서 비롯된다.
세 사례를 나란히 놓으면 공통된 패턴이 보인다. 버그의 진짜 원인은 항상 코드 바깥에 있었다. CSS 스펙이 만들어내는 컨텍스트, 사용자가 화면을 얼마나 확대했는가, 스케줄러가 가정한 그리드의 구조적 한계. 모두 코드를 읽는 것만으로는 발견할 수 없는 것들이다. 발견의 실마리는 항상 사용자가 실제로 처한 맥락에 있었다.
이 관점은 디버깅 방법론 이상의 의미를 갖는다. 프론트엔드 개발자가 UI 인터랙션을 설계할 때, '내 환경에서 동작하는가'보다 '사용자가 실제로 사용하는 환경에서 동작하는가'를 먼저 질문해야 한다는 태도의 문제이기도 하다. Modal 안에서 DnD를 쓸 수 있는가, 화면을 확대한 사용자도 동일하게 인터랙션할 수 있는가, 빽빽한 캘린더를 가진 사용자도 예약 가능한 시간을 찾을 수 있는가. 이 질문들은 모두 기술적 구현 이전에, 사용자 여정을 충분히 상상했는가의 질문이다.
앞으로 UI 인터랙션 버그를 마주할 때 한 가지를 먼저 해보길 권한다. '내 화면에서 재현되는가'를 묻기 전에 '사용자는 어떤 맥락에서 이 화면을 보고 있는가'를 먼저 물어보는 것. 재현이 안 된다는 건 버그가 없다는 게 아니라, 내가 사용자의 맥락을 아직 모른다는 신호일 수 있다.