매출 데이터의 날짜와 시간대를 맞추는 방법
한국 날짜와 UTC 날짜로 집계하면 주문 수는 같아도 매출이 달라질 수 있습니다. 자정 경계, GA4 시간 필드, Python 시간대 변환과 해외 캠페인의 23시간·25시간 하루를 합성 데이터와 검증 코드로 확인합니다.
두 리포트 모두 9월 2일 주문이 2건입니다. 그런데 매출은 한쪽이 50,000원이고 다른 쪽이 70,000원입니다. 합계 오류를 찾기 전에 어느 주문이 포함됐는지 확인하면 차이를 설명할 수 있습니다. 한쪽은 한국 날짜, 다른 쪽은 UTC 날짜로 집계했습니다.
아래는 시간대와 날짜 경계를 확인하기 위한 합성 주문 4건입니다. 중복을 제거한
단일 상점의 정상 주문이며, 금액은 KRW입니다. paid_at은 이 예제에서 실제 결제
시각을 뜻합니다. GA4 내보내기의 시간 필드를 그대로 복제한 데이터가 아닙니다.
주문 수가 같아도 날짜별 매출이 다른 이유
| 주문 | UTC 결제 시각 | 한국 결제 시각 | 금액 |
|---|---|---|---|
| o301 | 9월 1일 14:59:59.999999 | 9월 1일 23:59:59.999999 | 10,000원 |
| o302 | 9월 1일 15:00:00 | 9월 2일 00:00:00 | 20,000원 |
| o303 | 9월 2일 14:59:59.999999 | 9월 2일 23:59:59.999999 | 30,000원 |
| o304 | 9월 2일 15:00:00 | 9월 3일 00:00:00 | 40,000원 |
한국 날짜의 9월 2일에는 o302와 o303이 들어가서 50,000원이 됩니다. UTC 날짜의 9월 2일에는 o303과 o304가 들어가므로 70,000원입니다. 두 리포트 모두 주문은 2건입니다. 건수가 같다는 검증만으로는 집계 조건이 같다는 사실을 증명하지 못합니다.
자체 대시보드의 날짜 기준은 어느 쪽이든 정할 수 있습니다. 다만 매체 보고서, 주문 원장, 앱 분석에서 사용하는 날짜와 비교할 때는 시간대를 명시해야 합니다. 서버가 UTC를 쓴다는 이유만으로 마케터가 보는 일별 리포트까지 UTC 기준으로 해석되도록 두면 경계 근처의 차이가 반복됩니다.
하루의 시작과 끝을 어떻게 지정하나
한국 시간의 9월 2일은 9월 2일 00:00 이상, 9월 3일 00:00 미만으로 잡습니다.
끝 시각을 포함하지 않는 구간입니다. 수식에서 [시작, 끝)처럼 적는 반개구간을
사용하면 다음 날 자정의 주문이 두 날짜에 동시에 들어가지 않습니다.
한국 날짜 2026-09-02
[2026-09-02 00:00:00+09:00, 2026-09-03 00:00:00+09:00)
같은 구간을 UTC로 표현
[2026-09-01 15:00:00+00:00, 2026-09-02 15:00:00+00:00)
마지막 시각을 23:59:59로 두면 o303의 23:59:59.999999를 놓칠 수 있습니다. 저장 정밀도가 밀리초인지 마이크로초인지에 따라 ‘마지막으로 가능한 시각’을 계산하는 방식보다 다음 날의 시작을 제외하는 방식이 명확합니다.
예제에서 한국 날짜의 9월 1일·2일·3일 매출은 각각 10,000원·50,000원·40,000원입니다. 합계 100,000원이 원본 네 주문의 합계와 같고, 주문 ID도 중복 없이 모두 포함됩니다. 이처럼 기간을 나눈 뒤 다시 합쳐 보는 검증은 경계의 누락과 이중 집계를 찾는 데 도움이 됩니다.
Python에서 시간대를 바꾸는 방법
이미 UTC 시각이 있다면 astimezone으로 같은 순간을 한국 시각으로 표현합니다.
시간대 이름을 바꿔 붙이는 replace와는 동작이 다릅니다.
from datetime import datetime, timezone
from zoneinfo import ZoneInfo
source = datetime(2026, 9, 1, 15, tzinfo=timezone.utc)
print(source.astimezone(ZoneInfo("Asia/Seoul")).isoformat())
# 2026-09-02T00:00:00+09:00
print(source.replace(tzinfo=ZoneInfo("Asia/Seoul")).isoformat())
# 2026-09-01T15:00:00+09:00
두 번째 결과는 날짜와 시계 숫자를 그대로 두고 시간대만 바꿨습니다. UTC로 되돌리면
원본보다 9시간 이른 순간입니다. Python 공식 문서도 astimezone은 같은 UTC 순간을
유지하고, replace는 날짜·시각의 조정 없이 속성을 바꾸는 동작으로 설명합니다.
Python의 시간대 변환 설명
시간대 없는 2026-09-02 00:00:00 같은 원본은 출처가 어떤 기준으로 기록했는지
먼저 확인해야 합니다. 이미 한국 시각이라는 계약이 있다면 그 기준을 붙일 수 있지만,
임의로 UTC라고 가정하면 다른 순간을 만듭니다. 재현 예제는 오프셋 없는 결제 시각을
오류로 처리합니다.
GA4의 event_date와 event_timestamp는 어떻게 읽나
GA4 내보내기 스키마는 event_date를 등록된 시간대 기준의 기록 날짜로,
event_timestamp를 Google Analytics가 이벤트를 받은 시각의 UTC 마이크로초 값으로
설명합니다. 주문 원장의 paid_at과 같은 순간을 뜻하는지는 별도로 확인해야 합니다.
열 이름에 time이 들어 있다는 이유만으로 결제 시각을 대신하게 둘 수는 없습니다.
GA4 BigQuery 내보내기 스키마
BigQuery에서 UTC 마이크로초 값을 날짜로 바꿀 때는 단위와 시간대를 모두 드러낼 수 있습니다. 다음은 열 표현식의 예시입니다.
DATE(TIMESTAMP_MICROS(event_timestamp), 'Asia/Seoul')
TIMESTAMP_MICROS는 UTC 기준 epoch 마이크로초를 시각으로 해석하고, DATE의
두 번째 인자는 날짜를 읽을 시간대를 지정합니다. 초·밀리초·마이크로초를 혼동하면
문법에 맞는 코드라도 전혀 다른 날짜가 나올 수 있습니다.
BigQuery TIMESTAMP_MICROS,
BigQuery DATE
이 표현식만으로 GA4 보고서의 날짜와 모든 경우에 일치한다고 단정할 수는 없습니다. 시간 필드가 가리키는 시점, 속성 설정, 보고 지표와 원천의 범위를 함께 확인해야 합니다. 여기서는 공식 문서에 표현식을 대조했으며 클라우드 쿼리를 직접 실행한 결과로 제시하지 않습니다.
해외 캠페인의 하루는 항상 24시간인가
현지 달력의 하루와 실제 경과 시간이 달라질 수 있습니다. 예제 환경의 시간대 데이터로 뉴욕의 2026년 3월 8일을 계산하면 하루가 23시간이고, 11월 1일은 25시간입니다. 일광절약시간으로 UTC 오프셋이 바뀌기 때문입니다.
11월 1일에는 현지의 01:30이 두 번 나타납니다. 01:30-04:00과 01:30-05:00은
시계 숫자가 같지만 다른 순간입니다. 오프셋을 지우고 날짜·시각 문자열만 사용하면
두 주문을 같은 시점으로 오해할 수 있습니다. Python의 zoneinfo는 이런 반복
구간을 구분하는 fold를 지원하며, UTC에서 변환할 때 해당 구분을 적용합니다.
Python zoneinfo와 반복되는 시각
경과 시간을 계산할 때도 주의가 필요합니다. Python에서 같은 tzinfo를 가진 두
datetime을 바로 빼면 시간대 조정 없이 벽시계 기준의 차이를 계산합니다. 이 예제는
양쪽 날짜 경계를 UTC로 바꾼 뒤 차이를 구해 23시간과 25시간을 확인했습니다.
Python datetime 뺄셈 규칙
시간당 구매 수나 초당 요청 수처럼 경과 시간이 분모에 들어가는 지표라면 이 차이가 계산에 영향을 줍니다. 반면 현지 날짜별 매출을 보고 싶다는 업무 정의가 있다면 23시간·25시간인 날도 그 달력 날짜로 묶습니다. 모든 일별 지표를 일괄적으로 24시간에 맞춰 보정하는 방식은 목적에 맞지 않을 수 있습니다.
같은 결과를 직접 재현하는 방법
날짜·시간대 예제 ZIP을 풀고 폴더 안에서 실행합니다. Python 3.9 이상과 IANA 시간대 데이터가 필요하며, 검증에는 Python 3.11.7과 시스템 tzdata 2026c를 사용했습니다.
python3 analysis.py
python3 -m unittest
출력에는 한국 날짜의 2건·50,000원과 UTC 날짜의 2건·70,000원이 각각 포함됩니다. 뉴욕의 두 날짜는 거래 데이터 없이 날짜 경계의 경과 시간만 계산합니다. 테스트는 시작·끝 자정, 마이크로초 경계, 인접 날짜의 무중복 분할, 반복되는 현지 시각, 오프셋 없는 입력, 마이크로초 단위 변환 등을 확인합니다.
예제의 핵심 집계 조건은 다음과 같습니다. start와 end는 해당 지역의 날짜
경계를 UTC로 환산한 값입니다.
if start <= utc_instant(row["paid_at"]) < end:
selected.append(row)
시간대 규칙에는 역사적인 날짜 변경도 있습니다. 예제는 존재하지 않거나 두 가지로 해석되는 자정을 발견하면 별도 정책이 필요하다는 오류를 냅니다. 모든 지역의 역사적 변경을 임의의 기준으로 보정하는 코드는 아닙니다. 원본 시각과 적용한 시간대 데이터 버전을 보관하면 규칙이 바뀌었을 때 재현 범위를 확인할 수 있습니다.
날짜별 리포트에는 기준 시간대, 사용한 시각 필드, 시작·끝의 포함 규칙을 함께 남겨 보세요. 집계 건수가 같더라도 실제 주문 목록까지 맞춰 보면 경계에서 생긴 매출 차이를 놓치지 않을 수 있습니다.
공식 문서와 실행 환경은 2026년 9월 6일 확인했습니다.
마케팅 데이터 모델·품질·지표 정의 연재 3편입니다. 이전 글: 구매 이벤트 중복을 제거할 때 transaction_id부터 확인하는 이유
🐹
자주 묻는 질문
UTC 날짜를 한국 날짜처럼 사용하면 어떤 문제가 생기나요?
날짜 경계 근처의 주문이 다른 날에 포함될 수 있습니다. 합성 예제에서는 두 방식 모두 주문이 2건이지만 포함된 주문이 달라 매출이 50,000원과 70,000원으로 나뉩니다. 건수뿐 아니라 집계 대상 주문도 비교해야 합니다.
하루의 마지막 시각을 23시 59분 59초로 지정해도 되나요?
원본에 소수 초가 있다면 그 이후의 기록을 놓칠 수 있습니다. 해당 날짜의 시작 시각 이상, 다음 날짜의 시작 시각 미만으로 조건을 만들면 저장 정밀도에 맞춰 마지막 시각을 추측할 필요가 없습니다.
Python에서 replace와 astimezone은 같은 기능인가요?
다릅니다. astimezone은 같은 순간을 다른 시간대의 시각으로 표현합니다. replace로 tzinfo만 바꾸면 시계에 표시된 숫자는 유지되므로, 이미 시간대가 있는 시각을 다른 순간으로 바꿀 수 있습니다. 시간대 없는 원본에 기준을 붙일 때도 출처의 시간대 계약이 필요합니다.
해외 캠페인의 하루도 무조건 24시간으로 계산하나요?
일광절약시간 전환이나 지역의 시간대 규칙에 따라 달라질 수 있습니다. 예제는 현지 날짜의 시작과 다음 날짜의 시작을 각각 구한 뒤 UTC로 환산해 실제 경과 시간을 계산합니다. 시간대 데이터의 버전도 재현 조건에 포함합니다.