[wandr] 개발 일지 - 일정 화면 다듬기: 실사용하며 만난 자잘한 버그들
일정 화면을 새 구조로 갈아엎은 뒤, 며칠 실제로 써보면서 그동안 안 보이던 문제들이 하나둘 튀어나왔다. 큰 기능 버그는 아니고 다 "쓰다 보니 거슬리는" 종류였는데, 원인을 파고들면 하나같이 흔히 놓치는 지점들이었다. 하루짜리 버그 사냥 기록.
1. 서버 값이 다시 내려오면 타이핑 중이던 칸이 지워진다
Realtime으로 서버 값이 갱신될 때마다 로컬 입력을 전부 서버 값으로 다시 맞추는 코드가 있었다. 문제는 한 칸을 커밋하고 바로 다음 칸을 타이핑하는 순간이었다 — 앞서 보낸 수정 요청의 응답이 뒤늦게 도착해서 갱신 신호를 한 번 더 울리면, 지금 막 타이핑 중인 칸까지 서버 값으로 덮어써버렸다.
고친 방법은 간단했다. "로컬 값이 마지막으로 맞춰둔 서버 값과 다르면 지금 편집 중이라는 뜻"이라고 판단해서, 그 칸만 갱신 대상에서 빼는 것. 손대지 않은 칸만 새 값으로 맞추니 최신 데이터도 반영되고 타이핑도 안 지워진다.
2. 시간 입력이 15시를 5시로 알아듣는다
네이티브 <input type="time">을 썼는데, 이 입력창의 AM/PM 표시 여부는 페이지 언어 설정이 아니라 브라우저·OS 로케일을 따라간다. 그래서 미국 로케일 환경에서는 12시간제 UI가 뜨고, 사용자가 "15"를 입력해도 5시(PM 안 누르면 AM 취급)로 잘못 해석되는 일이 벌어졌다.
네이티브 컴포넌트를 신뢰할 수 없어서 그냥 텍스트 입력으로 바꿨다. HHMM이든 HH:MM이든 다 받아서 정규화하고, 범위를 벗어나거나 형식이 이상하면 커밋하지 않고 되돌리는 작은 컴포넌트를 하나 만들었다.
function normalizeTime(raw: string): string | null {
const digits = raw.replace(/[^0-9]/g, "");
if (digits.length !== 4) return null;
const hh = Number(digits.slice(0, 2));
const mm = Number(digits.slice(2, 4));
if (hh > 23 || mm > 59) return null;
return `${String(hh).padStart(2, "0")}:${String(mm).padStart(2, "0")}`;
}값을 커밋하는 시점도 blur나 Enter로만 한정해서, 타이핑 중간에 어중간한 값이 부모 상태로 새는 일도 막았다.
3. 공유 모달이 좁은 화면에서 여백 없이 화면 끝에 붙는다
이건 원인을 찾는 데 좀 걸렸다. 공유 모달과 초대 모달에 max-w-md를 줬는데, 모달 컴포넌트 자체의 기본값이 max-w-[calc(100%-2rem)](화면 양옆에 최소 여백을 보장하는 값)였다. 둘 다 "최대 너비"를 다루는 규칙이라 클래스 병합 라이브러리가 같은 그룹으로 보고, 나중에 적용된 max-w-md가 기본값을 완전히 밀어냈다. 그 결과 화면이 md 브레이크포인트보다 좁을 때는 최소 여백 자체가 사라졌다.
고치는 법은 접두어 하나 붙이는 것뿐이었다: sm:max-w-md. 이러면 좁은 화면에서는 기본값이 살아있다가, 화면이 넓어지고 나서야 내가 원하는 값으로 바뀐다. 클래스 병합 도구를 쓸 때는 "같은 그룹의 클래스는 나중 게 이긴다"는 걸 계속 의식해야 한다는 걸 다시 깨달았다.
4. 긴 URL이 모달 밖으로 삐져나온다
공유 링크처럼 줄바꿈 없는 긴 문자열을 모달 안에 넣었더니, 모달 크기는 그대로인데 텍스트만 바깥으로 삐져나오는 증상이 있었다. truncate를 걸어놨는데도 안 먹혔다.
원인은 CSS Grid의 기본 동작이었다. 모달 레이아웃이 grid였는데, 그리드 아이템의 기본 min-width는 auto라서 안에 있는 내용물의 "고유 크기"만큼은 절대 안 줄어든다. 줄바꿈 없는 긴 문자열의 고유 크기는 그 문자열 전체 길이라서, 그리드 열이 억지로 늘어나고 그 안의 truncate는 애초에 발동할 기회조차 없었다.
grid-template-columns: minmax(0, 1fr);이렇게 최솟값을 명시적으로 0으로 내려주면 아이템이 진짜로 줄어들 수 있게 되고, 그제서야 truncate가 제 역할을 한다. Flexbox의 min-width: auto 함정은 꽤 알려져 있는데 Grid에도 똑같이 있다는 걸 이번에 직접 겪었다. 그리고 이 증상은 페이지 자체에 가로 스크롤을 만들지 않는다(모달이 고정 위치라) — 그래서 "문서에 가로 스크롤 있나"만 확인하던 기존 테스트로는 못 잡았다. 자식 요소가 실제로 부모 경계를 넘는지 좌표를 직접 재는 검사를 추가했다.
5. AI 제안을 검토하다 말고 닫으면 크레딧이 조용히 사라진다
가장 마음에 걸렸던 버그. 빈 시간을 채워주는 AI 마법사가 제안을 몇 개 보여주는데, 사용자가 그중 일부만 보고 모달을 닫아버리면(ESC든 바깥 클릭이든) 그 요청에 쓴 크레딧이 그대로 날아갔다. 사용자 입장에서는 "돈 냈는데 아무것도 못 받은" 셈이다.
닫으려는 모든 경로를 하나의 함수로 모으고, 아직 검토 안 한 제안이 남아있으면 "닫으면 사라집니다"라는 확인을 한 번 더 보여주도록 고쳤다. 실수로 날리는 걸 막는 최소한의 안전장치다. 겸사겸사 마지막 제안까지 다 처리했는데 폼 화면으로 도로 돌아가버리던 것도 같이 고쳤다 — 폼 작성 중 / 제안 검토 중 / 다 끝남, 이렇게 단계를 명확히 나누는 상태 하나를 추가했더니 자연스럽게 풀렸다.
이번에 고친 다섯 가지 다 "기능은 되는데 실제로 쓰면 거슬리는" 것들이었다. 자동화 테스트로는 잘 안 걸리고, 직접 써봐야만 보이는 종류. 새 화면을 만들고 나서 며칠은 그냥 사용자처럼 써보는 시간이 꼭 필요하다는 걸 다시 확인했다.