Cheondi
개발 · API

조회 기간 변경과 이전 결과

날짜 범위를 빠르게 바꿀 때 오래된 응답이 최신 목록을 덮는 문제를 추적한 기록입니다.

  • #api
  • #async
  • #date-range

거래 내역 화면에서 기간을 한 달로 바꿨다가 바로 일주일로 바꾸면 가끔 한 달치 결과가 나타났다. 날짜 버튼에는 일주일이 선택돼 있어서 화면과 목록이 서로 다른 말을 했다. 서버 응답의 날짜가 잘못됐다고 생각했지만 각 요청 결과는 모두 정확했다.

문제는 한 달 요청이 더 느리게 끝나 마지막에 화면을 덮는 것이었다. 사용자의 최신 선택과 네트워크의 완료 순서는 같지 않았다.

응답까지 유지되는 요청 조건

요청을 보낼 때 현재 화면 값을 나중에 다시 읽지 않고 그 순간의 범위를 불변 값으로 만들었다.

public record DateRange(DateTimeOffset From, DateTimeOffset To);

async Task LoadAsync(DateRange requested)
{
    var result = await api.LoadHistory(requested);
    if (requested != currentRange) return;
    Render(result);
}

응답 시점에 요청 조건과 현재 조건이 같은지 비교하니 오래된 결과를 버릴 수 있었다.

날짜 범위의 종료 경계

2024-08-31까지라는 조건을 23시 59분 59초로 만들면 더 작은 시간 단위가 생길 때 누락될 수 있었다. 내부에서는 시작 이상, 다음 날 시작 미만의 반열린 구간으로 다뤘다.

2024-08-01 포함 <= timestamp < 2024-09-01 제외

사용자 시간대에서 선택한 날짜를 서버 기준 시각으로 변환하는 위치도 한 곳으로 모았다.

요청별 로딩 상태

첫 요청이 끝나며 로딩을 끄면 두 번째 요청이 아직 진행 중인데도 빈 화면처럼 보였다. 현재 요청 ID와 일치할 때만 로딩 상태를 종료했다. 새 요청이 시작되면 이전 요청은 가능하면 취소하고, 취소할 수 없어도 결과를 적용하지 않았다.

오류 메시지도 오래된 요청의 실패가 최신 성공 위에 나타나지 않도록 같은 기준을 사용했다.

필터 UI와 조회 데이터의 단일 상태

예전에는 날짜 버튼과 목록을 별개로 갱신했다. 이번에는 현재 조회 조건, 로딩, 결과, 오류를 하나의 화면 상태로 생각했다. 화면에 보이는 필터가 어떤 결과를 만들었는지 연결이 분명해졌다.

비동기 문제는 코드가 동시에 실행돼서만 생기는 게 아니었다. 서로 다른 시간에 시작한 올바른 결과 중 무엇이 현재 화면의 것인지 정하지 않은 게 원인이었다. 이후 검색이나 필터 API를 붙일 때는 요청 조건과 응답 적용 조건을 함께 설계하게 됐다.

추가 구현 기준

작업 당시에는 눈앞의 오류를 먼저 고쳤지만, 이후 기술 문서와 맞춰 보며 확인 순서를 조금 더 구체화했다.

RFC 3339의 문자열은 시각과 UTC 오프셋을 교환하는 데 유용하지만, 지역의 시간대 규칙 전체를 담는 형식은 아니다. +09:00은 그 순간의 차이를 말할 뿐 이후의 서머타임 규칙이나 지역 이름을 대신하지 않는다.

화면에서 필요한 것은 보통 서버가 기록한 절대 시각, 사용자가 선택한 지역, 표시 형식 세 가지다. 이 값을 하나의 문자열로 뭉치지 않으면 조회 범위와 표시 날짜가 어긋나는 문제를 줄일 수 있다.

다음에 조회 기간 변경과 이전 결과 같은 문제를 보면 아래 순서부터 확인하려고 한다.

  • 서버 저장 시각과 화면 지역 분리
  • 조회 시작·끝의 포함 여부
  • 날짜 변경선과 DST 경계 예제
  • 다운로드 실패 시 마지막 정상 규칙

날짜만 보내야 할 때와 정확한 순간을 보내야 할 때를 API 계약에서 구분하고, 범위의 시작·끝 포함 여부도 함께 고정해야 한다.

공식 참고 자료