개발 slecs

결제 도메인 날짜 선택기에 공통 캘린더 위젯 도입

목차

결제 도메인 date-picker 영역에 공통 캘린더 위젯을 붙이는 작업을 했다. 변경된 파일은 뷰/스타일 5개, SQL 매퍼 1개. 숫자만 보면 작아 보이지만, 금융 도메인에서 날짜 선택이 어디에 물려 있는지 생각하면 건드리기 쉬운 작업이 아니었다.

왜 native input을 걷어냈나

브라우저 기본 <input type="date">는 겉으로는 간단해 보이지만 결제 화면에서 쓰기엔 빈틈이 많다.

첫째로 브라우저/OS마다 UI가 달라서 디자인 통제가 안 된다. 크롬, 사파리, 엣지가 제각각 다른 달력을 띄운다. 사용자 입장에서 같은 서비스인데 화면이 달라 보이면 신뢰감이 떨어지고, 운영 입장에서는 QA 케이스가 브라우저별로 늘어난다.

둘째로 유효성 검증 커스텀이 어렵다. 결제 도메인에선 "오늘 이후 날짜는 선택 불가", "정산 기간은 최대 90일", "특정 날짜 범위 비활성화" 같은 조건이 흔한데, native input으로는 min/max 속성 정도가 한계다. 비즈니스 규칙이 조금만 복잡해지면 검증 로직이 컴포넌트 밖 여러 곳에 흩어지게 된다. 그게 쌓이면 수정할 때 하나 고치고 두 개 놓치는 상황이 생긴다.

셋째로 locale 처리. 브라우저 언어 설정에 따라 날짜 포맷이 달라지면 서버로 보내는 값 형식도 들쭉날쭉해질 수 있다. 결제 시스템에서 날짜 포맷이 서버와 어긋나면 조회 결과가 빈 화면이 되거나, 더 나쁜 경우 잘못된 데이터가 올라온다.

자체 캘린더 위젯을 만들어 공통화하기로 한 이유가 여기 있다. 한 번만 고치면 어디서든 반영된다는 점이 핵심이고, 날짜 포맷 규칙이나 선택 제약 조건이 바뀌면 각 페이지를 다 뜯지 않아도 된다. 공통화가 없으면 같은 버그가 여러 페이지에 조용히 살아남는 경우가 생긴다.

// 위젯 초기화 옵션 예시 (실제는 내부 클래스 기반이지만 개념은 동일)
const picker = new CommonDatePicker({
  el: '#payment-date',
  format: 'YYYY-MM-DD',
  minDate: null,
  maxDate: today(),
  disabledDates: closedDates,   // 휴일, 정산 마감일 등 비즈니스 규칙 주입
  rangeMax: 90,                 // 최대 선택 범위 일수
  onChange: (val) => updateQuery(val),
});

주요 페이지에서 native를 이 위젯으로 교체하면서 뷰/스타일 파일 5개가 바뀌었다. 단순 find-and-replace가 아니라 각 페이지의 기존 이벤트 바인딩 방식이 달라서 케이스별로 맞췄다.

SQL 매퍼와 날짜 정합성

UI 위젯만 바꾸면 끝날 것 같지만, 날짜 선택 범위가 바뀌면 그게 쿼리까지 내려간다. 이번에 SQL 매퍼도 함께 수정한 이유가 여기 있다. 기존 API가 내려주지 않던 데이터나 동작이 필요해졌고, 단순히 파라미터 하나를 추가하는 게 아니라 집계 쿼리 자체를 손봐야 했다.

결제 도메인 쿼리에서 날짜 조건을 다룰 때 신경 쓴 것들:

고려 항목 이유
타임존 기준 명시 서버/DB/클라이언트 타임존이 다를 때 경계값이 어긋남
BETWEEN vs >= AND < 끝 날짜 포함 여부. 하루 단위면 23:59:59 또는 익일 00:00:00 명시
NULL 처리 날짜 미입력 시 전체 조회 허용인지 차단인지 명확히
인덱스 활용 여부 날짜 칼럼에 함수 씌우면 인덱스 안 탐, 실행계획 확인 필수

BETWEEN의 경계값 처리는 팀마다 관행이 달라서 기존 코드가 어떤 방식인지 먼저 파악했다. 결과적으로 포맷을 YYYY-MM-DD 00:00:00 / YYYY-MM-DD 23:59:59로 명시하는 쪽으로 통일했다. "날짜가 맞는 것 같은데 결과가 하루씩 밀린다"는 버그는 이 경계값 처리가 제각각이어서 생기는 경우가 많다. 쿼리 레벨까지 함께 설계하고 나서, 화면 숫자와 DB 실제값이 일치하는지 cross-check할 때 의심 가는 지점이 눈에 띄게 줄었다.

쿼리 실행 계획도 사전에 확인했다. 날짜 범위 조건이 있는 집계 쿼리는 데이터가 쌓일수록 병목이 될 수 있어서, 인덱스를 타는지 확인하고 칼럼에 함수를 씌우는 부분은 걷어냈다.

구현하면서 챙긴 것들

기존 로직과의 충돌 확인이 생각보다 손이 갔다. 내부 클래스에 메서드를 추가할 때 기존 날짜 처리 흐름을 먼저 추적했는데, 같은 파라미터 이름을 다른 의미로 쓰는 지점이 하나 있어서 거기서 잠깐 멈칫했다. 새 기능을 추가할 때 자주 겪는 패턴인데, 기존 코드가 암묵적으로 가정하던 것들이 있어서 그걸 명시적으로 정리하거나 방어 코드를 넣어야 할 타이밍이 생긴다.

이번에 방어한 예외 케이스:
- 날짜 미선택 상태로 조회 요청이 들어올 때 - 전체 조회 허용인지 차단인지 결정하고 통일
- 시작일이 종료일보다 늦은 경우 위젯 단에서 선택 자체를 막음
- 권한 없는 사용자가 특정 날짜 범위를 조회하려 할 때 SQL 레벨에서 필터
- 빠른 연속 클릭으로 중복 요청이 발생하는 케이스 디바운스 처리

구현 후에는 수정 전에 메모해 둔 수치와 동일한 케이스로 다시 확인했다. 관련 화면들에서 숫자가 같은지도 봤다. 금융 도메인 특성상 "비슷해 보인다"로 넘어가면 나중에 반드시 다시 돌아온다는 걸 이미 경험으로 알고 있어서, 숫자 cross-check는 루틴처럼 한다. "대충 맞는 것 같다"는 감각을 믿으면 안 되는 도메인이다.

작업 단위로 커밋을 쪼갰다. 뷰 변경, 매퍼 수정, 공통 위젯 등록을 따로 커밋했다. 나중에 어떤 커밋에서 뭔가 깨졌는지 좁혀야 할 때 이 습관이 시간을 아껴준다. 커밋 메시지는 "무엇을 바꿨다"보다 "왜 바꿨는지"를 담으려고 계속 노력 중이다. 보름 뒤에 내가 다시 봤을 때, 그리고 팀원이 리뷰할 때 의도가 바로 읽혀야 하니까.

사내 서비스를 만들다 보면 기능 하나가 버튼 하나 추가하는 것으로 끝나지 않는다는 걸 계속 체감한다. SQL 집계, 상태 처리, 예외 방어, 화면 렌더링, 권한 체크가 다 엮여 있어서 어느 하나만 빠뜨려도 숫자가 맞지 않거나 특정 사용자에게 이상한 화면이 나타난다. 이번 작업도 파일 수만 보면 작은데, 막상 다 연결해놓고 보면 건드린 레이어가 꽤 됐다. 그 사실을 매번 다시 확인하는 느낌으로 작업하는 게 금융/결제 도메인에서는 기본값이어야 한다고 생각한다.

댓글 0

첫 댓글 달아줘.