개요
날짜와 시간 처리는 사소한 차이로 버그를 만들기 쉽습니다. 이 글은 Django와 Python에서 시간을 다룰 때 혼동하기 쉬운 naive/aware, ISO8601, timestamp(UNIX epoch) 개념을 빠르게 훑어보고, 안전한 사용법을 예제로 정리합니다.
datetime의 두 얼굴: naive vs aware
datetime 객체는 시간대 정보 유무에 따라 두 가지로 나뉩니다.
-
naive: 시간대 정보가 없는 객체 (
tzinfo=None) -
aware: 시간대 정보가 포함된 객체 (
tzinfo=...)
연산 규칙은 간단합니다. 같은 타입에서만 덧셈, 뺄셈 연산이 가능합니다.
-
naive-naive✅ (가능) -
aware-aware(같은 시간대) ✅ (가능) -
naive-aware❌ (TypeError 발생)
from datetime import datetime, timezone
# naive: 시간대 정보가 없음
naive = datetime(2025, 11, 14, 9, 25, 1) # tzinfo=None
# aware: 시간대 정보(이 경우 UTC)를 명시
aware = datetime(2025, 11, 14, 9, 25, 1, tzinfo=timezone.utc)
_ = aware - datetime.now(timezone.utc) # OK (aware - aware)
# _ = aware - datetime.now() # TypeError (aware - naive)
Django의 timezone과 USE_TZ
Django의 settings.py에 있는 USE_TZ 설정이 중요합니다.
-
USE_TZ=True(권장):django.utils.timezone.now()가 UTC 기준의 aware 객체를 반환합니다. -
USE_TZ=False: 서버 로컬 시간 기준의 naive 객체를 반환합니다.
대부분의 웹 애플리케이션은 USE_TZ=True로 설정하고, 모든 내부 로직과 데이터베이스 저장은 UTC로 통일하는 것이 가장 안전합니다. 사용자에게 보여줄 때만 현지 시간으로 변환합니다.
from django.utils import timezone
# USE_TZ=True일 때, now()는 항상 aware UTC 객체를 반환
now = timezone.now()
ISO8601와 isoformat()
datetime.isoformat() 메서드는 ISO8601 표준 문자열을 생성합니다.
-
UTC aware 객체는 보통
2025-11-14T09:25:01+00:00처럼 오프셋(+00:00)을 포함합니다. -
Z는 UTC(+00:00)를 의미하는 또 다른 표기법입니다. (예:...T09:25:01Z)
중요한 함정: Python 표준 라이브러리의 datetime.fromisoformat()는 Z 접미사를 직접 파싱하지 못합니다. Z 표기법을 처리해야 한다면, Django의 django.utils.dateparse.parse_datetime()을 사용하는 것이 안전합니다.
from datetime import datetime, timezone
from django.utils.dateparse import parse_datetime
dt = datetime(2025, 11, 14, 9, 25, 1, tzinfo=timezone.utc)
s = dt.isoformat() # '2025-11-14T09:25:01+00:00'
_aware = datetime.fromisoformat(s) # OK
_aware2 = parse_datetime("2025-11-14T09:25:01Z") # OK (Django 유틸)
timestamp(UNIX epoch) 제대로 이해하기
Timestamp(타임스탬프) 는 1970-01-01 00:00:00 UTC (UNIX epoch) 시점부터 현재까지 경과한 시간을 초(second) 로 나타낸 값입니다.
-
정수부: 기준 시점부터의 "초"
-
소수부: 초 단위 이하의 정밀도 (마이크로초)
Python의 time.time()은 현재 UTC epoch 초를 반환합니다. datetime 객체의 .timestamp() 메서드도 동일한 UTC epoch 기준을 사용합니다.
import time
from datetime import datetime, timezone
# 현재 시각의 epoch (두 값은 사실상 같은 의미)
t1 = time.time()
t2 = datetime.now(timezone.utc).timestamp()
# 특정 시각 ↔ epoch 상호 변환
exp_ts = 1731576301.25 # 소수부 0.25초 (250ms)
exp_dt = datetime.fromtimestamp(exp_ts, tz=timezone.utc) # aware UTC
back_ts = exp_dt.timestamp() # 1731576301.25
Timestamp는 서버 간 데이터를 교환하거나, 만료 시간을 계산하는 등 수치 연산이 필요할 때 유용합니다. 시간대 문자열 파싱 문제를 피할 수 있습니다.
안전한 변환/계산 패턴 (예제)
1. 문자열(ISO8601) → datetime
Z 접미사나 다양한 오프셋을 안전하게 처리하기 위해 parse_datetime을 사용합니다.
from django.utils.dateparse import parse_datetime
from django.utils import timezone
s = "2025-11-14T09:25:01Z" # 또는 ...+00:00
dt = parse_datetime(s)
if dt is None:
raise ValueError("Invalid datetime string")
# 만약 파싱된 시간이 naive라면 (오프셋 정보가 없었다면)
if timezone.is_naive(dt):
# 비즈니스 규칙에 맞게 명시적으로 시간대를 지정 (예: UTC로 간주)
from zoneinfo import ZoneInfo
dt = timezone.make_aware(dt, ZoneInfo("UTC"))
2. datetime(aware) ↔ timestamp
항상 timezone.utc를 기준으로 변환하는 것이 명확합니다.
from datetime import datetime, timezone
dt = datetime(2025, 11, 14, 9, 25, 1, tzinfo=timezone.utc)
ts = dt.timestamp() # float
dt2 = datetime.fromtimestamp(ts, tz=timezone.utc) # 상호 변환 OK
3. 만료까지 남은 시간(초) 계산
두 aware 객체를 빼서 timedelta를 얻은 후 .total_seconds()를 사용합니다.
from datetime import datetime, timezone
exp = datetime(2025, 11, 14, 9, 25, 1, tzinfo=timezone.utc)
now = datetime.now(timezone.utc)
remaining = (exp - now).total_seconds() # float seconds (음수 가능)
핵심: 모든 연산은 aware 객체(가급적 UTC) 끼리 수행하는 것이 가장 안전합니다. 주의사항 : datetime()의 tz|tzinfo 매개변수에 입력하는 timezone.utc는 django.utils.timezone이 아닌 datetime.timezone 입니다.
흔한 함정과 빠른 회피
-
aware-naive연산:TypeError가 발생합니다.- → 둘 다
aware(가급적 UTC)로 통일하거나,naive로 통일하되 의미를 정확히 인지하고 사용해야 합니다.
- → 둘 다
-
fromisoformat()와 "Z" 접미사:- → 표준 라이브러리는
Z를 지원하지 않습니다. Django의parse_datetime()을 사용하거나,replace("Z", "+00:00")처리가 필요합니다.
- → 표준 라이브러리는
-
로컬 시간 의존 (
datetime.now()):- → 서버 배포 환경(로컬, 개발 서버, 프로덕션)에 따라 시간이 달라져 버그를 유발합니다. 내부 로직은 항상 UTC(
timezone.now()) 기준으로 작성해야 합니다.
- → 서버 배포 환경(로컬, 개발 서버, 프로덕션)에 따라 시간이 달라져 버그를 유발합니다. 내부 로직은 항상 UTC(
결론
-
시간 연산의 제1 원칙은
naive와aware를 섞지 않는 것입니다. 가급적 aware(UTC) 로 통일하세요. -
문자열을 다룰 땐 ISO8601 형식을 사용하되, 파싱은
Z처리가 가능한parse_datetime()을 우선 고려하세요. -
만료 시간 계산 등 수치 연산이 필요하면 timestamp를 활용하세요.
댓글이 없습니다.