사용자가 오늘을 선택했는데 어제 마지막 내역이 섞이거나 오늘 첫 내역이 빠지는 문제가 있었다. 날짜 선택 UI는 정상이고 서버도 요청받은 범위를 정확히 조회했다. 서로 생각하는 하루의 시작이 달랐다. 화면은 사용자 시간대의 자정을 기준으로 했고 서버 요청은 UTC 날짜 문자열을 그대로 사용했다.
날짜는 달력의 칸이고 시각은 시간대가 포함된 순간이라는 차이를 다시 봐야 했다.
사용자 날짜의 조회 구간 변환
선택한 시작일과 종료일을 사용자 시간대에서 시각 구간으로 만든 뒤 UTC로 변환했다.
DateTime localStart = selectedFrom.Date;
DateTime localEndExclusive = selectedTo.Date.AddDays(1);
DateTime utcStart = TimeZoneInfo.ConvertTimeToUtc(localStart, userZone);
DateTime utcEnd = TimeZoneInfo.ConvertTimeToUtc(localEndExclusive, userZone);
서버에는 utcStart <= timestamp < utcEnd 형태로 전달했다. 종료일의 23:59:59를 만드는 방식보다 정밀도 변화에 안전했다.
시간대에 따른 하루 길이 변화
일광절약시간이 적용되는 지역에서는 하루가 23시간이나 25시간일 수 있었다. AddHours(24)로 다음 날을 계산하지 않고 해당 시간대의 다음 날짜 자정을 다시 변환했다.
존재하지 않거나 두 번 나타나는 로컬 시각을 입력으로 받을 때의 정책도 확인했다. 날짜 단위 조회에서는 자정 경계가 유효한지 시간대 데이터에 따라 처리했다.
화면 표시와 서버 필터의 공통 시간 기준
테스트 데이터는 경계 바로 전과 직후에 만들었다.
시작 1밀리초 전 -> 제외
시작 시각 -> 포함
종료 직전 -> 포함
다음 날 시작 시각 -> 제외
서버 응답을 다시 사용자 시간대로 표시했을 때 선택한 날짜 안에 들어오는지도 확인했다. 요청만 맞거나 화면만 맞는 것으로는 충분하지 않았다.
조회 계약의 시간대
처음 시간 문제를 겪었을 때는 출력 포맷에 집중했다. 날짜 범위 조회에서는 어떤 시간대의 날짜를 선택했는지가 API 조건 자체를 바꿨다. UI와 서버가 각자 올바른 계산을 해도 경계를 합의하지 않으면 결과는 틀렸다.
이후 날짜 필터를 만들 때 날짜 문자열만 넘기지 않는다. 사용자 시간대, 포함·제외 규칙, 서버 저장 기준을 계약으로 적는다. 달력 UI 하나에도 데이터 경계가 숨어 있었다.
추가 구현 기준
이 문제를 고친 뒤 관련 공식 자료를 찾아 읽으면서, 당시 코드에서 우연히 맞았던 부분과 규칙으로 남겨야 할 부분을 나눠 봤다.
RFC 3339의 문자열은 시각과 UTC 오프셋을 교환하는 데 유용하지만, 지역의 시간대 규칙 전체를 담는 형식은 아니다. +09:00은 그 순간의 차이를 말할 뿐 이후의 서머타임 규칙이나 지역 이름을 대신하지 않는다.
화면에서 필요한 것은 보통 서버가 기록한 절대 시각, 사용자가 선택한 지역, 표시 형식 세 가지다. 이 값을 하나의 문자열로 뭉치지 않으면 조회 범위와 표시 날짜가 어긋나는 문제를 줄일 수 있다.
다음에 날짜 범위·시간대 동시 처리 같은 문제를 보면 아래 순서부터 확인하려고 한다.
- 서버 저장 시각과 화면 지역 분리
- 조회 시작·끝의 포함 여부
- 날짜 변경선과 DST 경계 예제
- 다운로드 실패 시 마지막 정상 규칙
날짜만 보내야 할 때와 정확한 순간을 보내야 할 때를 API 계약에서 구분하고, 범위의 시작·끝 포함 여부도 함께 고정해야 한다.
공식 참고 자료
- RFC 3339 날짜·시간 형식 — 오프셋을 포함한 인터넷 날짜·시간 표현