Ari.dev

막힌다는 신고를 따라가다 반대 방향 결함을 찾았다 — 캘린더 버그 3중 원인 추적기

체크아웃 날짜가 선택되지 않는다는 신고 하나를 따라가다, 같은 코드에서 정반대 방향의 결함까지 발견했다. 판매 불가 날짜가 포함된 예약이 조용히 통과하고 있었다. 세 겹으로 얽힌 원인을 하나씩 걷어낸 과정.

트러블슈팅

관련 프로젝트: 예약 검색 날짜 경계 결함 추적

"검색 캘린더에서 체크아웃 날짜가 선택되지 않는다"는 신고로 시작했다. 원인을 따라가다 보니, 같은 코드 안에 막혀야 하는데 안 막히는 반대 방향 결함까지 숨어 있었다. 그쪽이 훨씬 위험했다.

1층 — 재클릭이 구간을 역전시킨다

같은 날짜를 다시 클릭하면, "체크아웃은 최소 하루 뒤"라는 보정 로직이 시작일보다 이른 종료일을 만들어냈다. 날짜 라이브러리는 역전된 구간을 에러 없이 조용히 정렬해버렸고, 그 결과 정상 날짜가 선택 불가로 오탐됐다.

증상만 보면 "캘린더가 이상하다"였다. 하지만 캘린더 자체는 정직했다 — 역전된 구간을 받아서 정렬했을 뿐이다. 문제는 그 앞에서 역전된 구간을 만들어 넘긴 보정 로직이었다.

2층 — 거부된 클릭도 라이브러리 안에서는 살아있다

앱 로직이 클릭을 거부해도(1층 결함 때문에), 캘린더 라이브러리 내부의 focus 상태는 그대로 진행됐다. 상태가 앱과 라이브러리 두 곳에서 따로 관리되고 있었기 때문이다.

앱은 "이 클릭은 무효"라고 판단해 자기 상태를 안 바꿨지만, 라이브러리는 그 판단을 모르고 자기 내부 포커스를 이미 옮겨놨다. 다음 클릭이 들어오면 엉뚱한 이전 포커스와 조합되어 예측 불가능한 결과가 나왔다.

고친 방법은 거부 시 캘린더를 리마운트해 라이브러리의 내부 focus만 초기화하는 것이었다. 선택 범위 상태(앱이 들고 있는 진짜 상태)는 건드리지 않았다 — 그래야 사용자에게 보이는 달이 이전 선택으로 되돌아가지 않는다.

3층 — 진짜 위험한 건 반대 방향이었다

1·2층을 고치던 중, 같은 코드 경로에서 정반대 방향의 결함을 발견했다. 공용 날짜 유틸이 "yyyy-MM-dd" 문자열을 UTC 자정으로 해석하고 있었는데, 캘린더 쪽에서 다루는 Date 객체는 로컬 자정 기준이었다.

// 개념 스케치 — 두 기준이 갈려 있던 지점 (실제 구현 아님)
parseDate("2026-08-10")        // UTC 자정으로 해석 → 로컬 기준 9시간 밀림
new Date(2026, 7, 10)          // 로컬 자정 기준

두 기준이 9시간 어긋나 있었다. 신고된 증상(선택이 막힌다)과는 정반대로, 이 결함은 막혀야 할 날짜를 통과시켰다. 판매 불가로 지정된 날짜가 경계 비교에서 9시간 밀리면서, 그 날짜가 포함된 예약이 그대로 진행될 수 있었다.

신고를 따라가지 않고 "일단 재현되는 증상만" 고쳤다면 이 결함은 발견되지 않았을 것이다. 증상은 "선택 안 됨"이었지만, 원인 코드를 들여다보는 과정에서 "선택이 잘못 허용됨"이라는 반대 방향 결함이 같은 자리에 있었다.

정리

원인증상 방향
1재클릭 시 보정 로직이 구간 역전 → 라이브러리가 조용히 정렬정상 날짜가 선택 불가 (신고된 증상)
2클릭 거부돼도 라이브러리 내부 focus는 진행다음 클릭이 엉뚱한 상태와 조합
3날짜 문자열 파싱이 UTC/로컬 자정으로 기준이 갈림판매 불가 날짜가 포함된 예약이 통과

3번은 캘린더 버그가 아니라 이 유틸을 쓰는 모든 호출부에 걸린 기존 결함이었다. 문자열 입력만 로컬 자정으로 맞추도록 앞단에 분기를 넣어 고치고, 이 유틸을 참조하는 호출부를 전부 훑어 회귀가 없는지 확인했다.

하나의 신고를 끝까지 따라가는 것과 "재현되는 증상만" 고치는 것의 차이가 여기서 갈렸다. 신고된 증상의 반대 방향에 무엇이 있는지 확인하지 않으면, 더 위험한 결함은 조용히 남는다.

댓글