본문 바로가기
언어별 개념 정리/Python

Python datetime에 시간대를 붙이면 시각도 바뀔까?

by char_lie 2026. 9. 18.
반응형

Python datetime 시간대 변환과 zoneinfo를 설명하는 썸네일

 

Python의 datetime에 시간대를 붙일 때 시각까지 변환하려면 astimezone()을 사용해야 한다. replace(tzinfo=...)는 벽시계 숫자를 그대로 둔 채 시간대 정보만 붙이므로, 이미 어떤 순간을 나타내는 datetime을 다른 지역 시각으로 바꾸는 용도로 사용하면 결과가 틀어진다.

핵심은 먼저 값의 의미를 구분하는 것이다. 시간대가 없는 naive datetime이 서울 현지 시각을 뜻한다면 replace(tzinfo=ZoneInfo("Asia/Seoul"))로 의미를 붙일 수 있다. 이미 UTC를 나타내는 aware datetime을 서울 시각으로 표시하려면 astimezone(ZoneInfo("Asia/Seoul"))으로 변환한다.

시간대 정보를 붙이는 경우

다음 문자열은 외부 시스템이 아니라 사용자가 서울 현지 시각으로 입력했다고 가정한다. 숫자 자체를 이동하지 않고 이 값이 어느 시간대 기준인지 지정한다.

from datetime import datetime
from zoneinfo import ZoneInfo

seoul = ZoneInfo("Asia/Seoul")
naive = datetime(2026, 9, 18, 9, 0)
aware = naive.replace(tzinfo=seoul)

print(naive)
print(aware)
print(aware.utcoffset())

실행 결과는 다음과 같다.

2026-09-18 09:00:00
2026-09-18 09:00:00+09:00
9:00:00

replace 전후의 09:00은 같다. 이 작업은 09:00을 다른 시각으로 계산한 것이 아니라 “이 09:00은 서울 시각”이라는 정보를 붙인 것이다. 원래 값이 실제로 UTC였는데 서울 시간대를 붙이면 동일한 순간보다 9시간 앞선 값으로 잘못 해석된다.

같은 순간을 다른 지역 시각으로 바꾸기

UTC aware datetime을 서울과 로스앤젤레스 시각으로 변환해 본다.

from datetime import datetime, timezone
from zoneinfo import ZoneInfo

utc_time = datetime(2026, 9, 18, 0, 0, tzinfo=timezone.utc)
seoul_time = utc_time.astimezone(ZoneInfo("Asia/Seoul"))
la_time = utc_time.astimezone(ZoneInfo("America/Los_Angeles"))

print(utc_time.isoformat())
print(seoul_time.isoformat())
print(la_time.isoformat())
print(utc_time.timestamp() == seoul_time.timestamp() == la_time.timestamp())

실행 환경의 시간대 데이터에 따라 2026년 9월 로스앤젤레스의 UTC 오프셋은 -07:00으로 계산된다.

2026-09-18T00:00:00+00:00
2026-09-18T09:00:00+09:00
2026-09-17T17:00:00-07:00
True

날짜와 시각 표시는 달라졌지만 세 값의 Unix timestamp는 같다. astimezone은 같은 순간을 대상 시간대의 벽시계 시각으로 표현한다. 날짜가 전날이나 다음 날로 바뀌는 것도 정상적인 변환 결과다.

naive datetime을 바로 변환하지 않기

naive.astimezone(target) 호출은 Python에서 동작할 수 있지만, naive 값을 시스템 로컬 시간대로 가정한다. 같은 코드를 UTC 서버와 한국 개발 환경에서 실행하면 출발 시간대가 달라져 결과도 달라질 수 있다.

출발 시간대를 알고 있다면 먼저 명시적으로 붙인 뒤 변환한다.

from datetime import datetime
from zoneinfo import ZoneInfo

source = datetime(2026, 9, 18, 9, 0)
source_aware = source.replace(tzinfo=ZoneInfo("Asia/Seoul"))
utc_time = source_aware.astimezone(ZoneInfo("UTC"))

print(utc_time.isoformat())
2026-09-18T00:00:00+00:00

API에서 2026-09-18T00:00:00Z처럼 오프셋이 포함된 문자열을 받았다면 파싱 단계에서 aware datetime으로 만드는 편이 안전하다. 데이터베이스나 외부 API가 시간대 없는 문자열을 반환한다면 그 값이 UTC인지 현지 시각인지 명세를 먼저 확인해야 한다.

서머타임으로 같은 시각이 두 번 나타날 때

일부 지역은 서머타임이 끝날 때 같은 벽시계 시각이 두 번 나타난다. fold는 이 둘을 구분한다. 2020년 11월 1일 로스앤젤레스의 01:30을 예로 들면 fold=0은 전환 전 오프셋, fold=1은 전환 후 오프셋을 사용한다.

from datetime import datetime
from zoneinfo import ZoneInfo

la = ZoneInfo("America/Los_Angeles")
first = datetime(2020, 11, 1, 1, 30, tzinfo=la, fold=0)
second = datetime(2020, 11, 1, 1, 30, tzinfo=la, fold=1)

print(first.isoformat(), int(first.timestamp()))
print(second.isoformat(), int(second.timestamp()))
print(int(second.timestamp() - first.timestamp()))
2020-11-01T01:30:00-07:00 1604219400
2020-11-01T01:30:00-08:00 1604223000
3600

화면의 날짜와 시각은 같지만 실제 순간은 한 시간 차이다. UTC 같은 다른 시간대에서 astimezone으로 변환하면 Python이 해당 순간에 맞는 fold 값을 설정한다. 사용자가 애매한 현지 시각을 직접 입력하는 시스템이라면 어느 쪽을 뜻하는지 추가 정보가 필요하다.

저장과 비교 기준 정하기

서비스 내부에서는 aware datetime을 유지하고, 저장·전송 형식에 UTC 오프셋을 포함하는 방법이 다루기 쉽다. 화면에 표시할 때만 사용자 지역 시간대로 바꾸면 날짜 경계와 서머타임 규칙을 한곳에서 처리할 수 있다.

from datetime import datetime, timezone

now_utc = datetime.now(timezone.utc)
payload = now_utc.isoformat()
print(payload)

서로 다른 시간대의 aware datetime은 같은 순간인지 비교할 수 있다. 반면 naive와 aware datetime의 순서 비교는 오류가 나므로 두 종류를 섞지 않는다. 테스트에서는 시스템 기본 시간대에 기대지 말고 입력과 예상값에 시간대를 모두 명시한다.

정리하면 replace(tzinfo=...)는 시간대 없는 벽시계 값의 의미를 알고 있을 때 붙이는 작업이고, astimezone(...)은 이미 시간대를 가진 순간을 다른 지역 시각으로 바꾸는 작업이다. 이 둘을 구분하면 서버 환경에 따라 시간이 밀리거나 날짜가 달라지는 문제를 줄일 수 있다.

반응형

댓글