Tutorial
일광 절약 시간 변경 처리: 튜토리얼 완료
소개
일광 절약 시간(DST) 전환은 타임스탬프 작업에서 가장 어려운 측면 중 하나입니다. 1년에 두 번, 시계가 "앞으로 튀어나오거나" "후퇴"하여 버그, 데이터 손실 및 잘못된 계산을 유발할 수 있는 극단적인 경우가 발생합니다. 이 튜토리얼에서는 애플리케이션에서 DST 전환을 올바르게 처리하는 방법을 알려줍니다.
DST 전환 이해
DST 중에는 어떤 일이 발생하나요?
스프링 포워드(DST 시작)
- 시계가 1시간 앞으로 이동합니다(일반적으로 오전 2시 → 오전 3시).
- 한 시간이 "빠졌습니다" - 오전 2시 30분과 같은 시간은 존재하지 않습니다.
- 기간: 하루는 단 23시간입니다.
폴백(DST 종료)
- 시계가 1시간 뒤로 이동합니다(일반적으로 오전 2시 → 오전 1시).
- 1시간이 "중복"됩니다. - 오전 1시 30분이 두 번 발생합니다.
- 기간: 하루는 25시간입니다.
실제 영향
// Spring Forward - March 10, 2024 (US)
// Problem: Scheduled task at 2:30 AM doesn't execute
const scheduled = new Date('2024-03-10T02:30:00');
// This time doesn't exist! JavaScript may interpret it as 3:30 AM
// Fall Back - November 3, 2024 (US)
// Problem: Log entries at 1:30 AM appear twice
const firstOccurrence = new Date('2024-11-03T01:30:00-04:00'); // EDT
const secondOccurrence = new Date('2024-11-03T01:30:00-05:00'); // EST
// Same wall clock time, different actual times!
일반적인 DST 문제
문제 1: 누락된 시간(봄 앞으로)
시계가 앞으로 나아갈 때 누락된 시간에 타임스탬프를 생성하려고 하면 예기치 않은 동작이 발생할 수 있습니다.
자바스크립트 동작
// March 10, 2024, 2:30 AM in New York doesn't exist
const date = new Date('2024-03-10T02:30:00');
console.log(date.toLocaleString('en-US', {
timeZone: 'America/New_York',
hour12: false
}));
// Output varies by browser/environment
// Most will interpret as 3:30 AM or 1:30 AM
Python 동작
from datetime import datetime
import pytz
ny_tz = pytz.timezone('America/New_York')
# Naive datetime in missing hour
try:
dt = ny_tz.localize(datetime(2024, 3, 10, 2, 30))
print(dt)
except pytz.exceptions.NonExistentTimeError as e:
print(f"Error: {e}")
# Error: 2024-03-10 02:30:00
해결책: 존재하지 않는 시간을 명시적으로 처리
# Option 1: Use is_dst parameter
dt = ny_tz.localize(datetime(2024, 3, 10, 2, 30), is_dst=None)
# Raises exception for ambiguous time
# Option 2: Normalize after creation
naive_dt = datetime(2024, 3, 10, 2, 30)
dt = ny_tz.normalize(ny_tz.localize(naive_dt, is_dst=False))
print(dt)
# 2024-03-10 03:30:00 EDT (adjusted forward)
문제 2: 시간 중복(대체)
시계가 뒤로 떨어지면 동일한 벽시계 시간이 두 번 발생하여 모호성을 유발합니다.
자바스크립트 예
// November 3, 2024, 1:30 AM in New York occurs twice
// First occurrence (before fall back)
const first = new Date('2024-11-03T01:30:00-04:00'); // EDT
// Second occurrence (after fall back)
const second = new Date('2024-11-03T01:30:00-05:00'); // EST
console.log(first.getTime()); // 1730616600000
console.log(second.getTime()); // 1730620200000
console.log(second - first); // 3600000 (1 hour difference)
파이썬 예제
from datetime import datetime
import pytz
ny_tz = pytz.timezone('America/New_York')
# Ambiguous time - which occurrence?
try:
dt = ny_tz.localize(datetime(2024, 11, 3, 1, 30))
except pytz.exceptions.AmbiguousTimeError as e:
print(f"Ambiguous: {e}")
# Specify which occurrence
first = ny_tz.localize(datetime(2024, 11, 3, 1, 30), is_dst=True) # Before fall back
second = ny_tz.localize(datetime(2024, 11, 3, 1, 30), is_dst=False) # After fall back
print(first) # 2024-11-03 01:30:00 EDT-0400
print(second) # 2024-11-03 01:30:00 EST-0500
문제 3: 잘못된 기간 계산
DST 전환은 달력 기반 산술을 사용할 때 기간 계산에 영향을 줍니다.
// Calculate hours between midnight on DST transition days
// Spring Forward Day (23 hours)
const springStart = new Date('2024-03-10T00:00:00');
const springEnd = new Date('2024-03-10T23:59:59');
const springHours = (springEnd - springStart) / 3600000;
console.log(springHours); // 23.999... hours (not 24!)
// Fall Back Day (25 hours)
const fallStart = new Date('2024-11-03T00:00:00');
const fallEnd = new Date('2024-11-03T23:59:59');
const fallHours = (fallEnd - fallStart) / 3600000;
console.log(fallHours); // 24.999... hours (appears normal but day is 25 hours)
DST 처리 모범 사례
1. 항상 시간대 인식 날짜/시간을 사용하세요.
date-fns-tz를 사용하는 JavaScript
import { zonedTimeToUtc, utcToZonedTime, format } from 'date-fns-tz';
// Always work in UTC internally
const utcDate = zonedTimeToUtc('2024-03-10 02:30', 'America/New_York');
// Convert to local only for display
const nyDate = utcToZonedTime(utcDate, 'America/New_York');
console.log(format(nyDate, 'yyyy-MM-dd HH:mm:ss zzz', { timeZone: 'America/New_York' }));
pytz를 사용한 Python
from datetime import datetime
import pytz
# Always work with timezone-aware datetimes
utc = pytz.UTC
ny_tz = pytz.timezone('America/New_York')
# Create timezone-aware datetime
dt_utc = datetime(2024, 3, 10, 7, 30, tzinfo=utc) # UTC time
dt_ny = dt_utc.astimezone(ny_tz) # Convert to NY time
print(dt_ny) # 2024-03-10 03:30:00 EDT (automatically adjusted for DST)
2. DST 전환 감지
자바스크립트: 날짜가 DST인지 확인하세요
function isDST(date, timezone) {
const jan = new Date(date.getFullYear(), 0, 1);
const jul = new Date(date.getFullYear(), 6, 1);
const janOffset = jan.getTimezoneOffset();
const julOffset = jul.getTimezoneOffset();
const stdOffset = Math.max(janOffset, julOffset);
const currentOffset = date.getTimezoneOffset();
return currentOffset < stdOffset;
}
const winterDate = new Date('2024-01-15T12:00:00');
const summerDate = new Date('2024-07-15T12:00:00');
console.log(isDST(winterDate)); // false
console.log(isDST(summerDate)); // true
Python: DST 전환 날짜 찾기
from datetime import datetime, timedelta
import pytz
def find_dst_transitions(year, timezone_name):
"""Find DST transition dates for a given year and timezone."""
tz = pytz.timezone(timezone_name)
transitions = []
# Check each day of the year
for day in range(365):
date = datetime(year, 1, 1) + timedelta(days=day)
today = tz.localize(datetime(year, 1, 1) + timedelta(days=day), is_dst=None)
tomorrow = tz.localize(datetime(year, 1, 1) + timedelta(days=day + 1), is_dst=None)
# Check if UTC offset changed
if today.utcoffset() != tomorrow.utcoffset():
transitions.append({
'date': date.strftime('%Y-%m-%d'),
'from_offset': str(today.utcoffset()),
'to_offset': str(tomorrow.utcoffset()),
'type': 'Spring Forward' if today.utcoffset() < tomorrow.utcoffset() else 'Fall Back'
})
return transitions
# Find 2024 DST transitions in New York
transitions = find_dst_transitions(2024, 'America/New_York')
for t in transitions:
print(f"{t['date']}: {t['type']} ({t['from_offset']} → {t['to_offset']})")
# Output:
# 2024-03-10: Spring Forward (-5:00:00 → -4:00:00)
# 2024-11-03: Fall Back (-4:00:00 → -5:00:00)
3. 누락된 시간을 적절하게 처리
자바스크립트: 유효한 시간으로 정규화
function normalizeToValidTime(dateString, timezone) {
try {
// Attempt to create date
const date = new Date(dateString);
// Check if time exists by comparing round-trip conversion
const formatted = date.toLocaleString('en-US', {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
// If times don't match, time was adjusted
return {
original: dateString,
normalized: date.toISOString(),
wasAdjusted: !dateString.includes(formatted.split(',')[1].trim())
};
} catch (error) {
return { error: error.message };
}
}
const result = normalizeToValidTime('2024-03-10T02:30:00', 'America/New_York');
console.log(result);
// { original: '2024-03-10T02:30:00', normalized: '2024-03-10T07:30:00.000Z', wasAdjusted: true }
Python: 명시적인 DST 처리
def safe_localize(tz, dt, prefer_dst=True):
"""
Safely localize datetime, handling DST transitions.
Args:
tz: pytz timezone
dt: naive datetime
prefer_dst: If True, prefer DST time during ambiguous hour
Returns:
Localized datetime
"""
try:
# Try to localize normally
return tz.localize(dt, is_dst=None)
except pytz.exceptions.AmbiguousTimeError:
# Ambiguous time (fall back) - specify preference
return tz.localize(dt, is_dst=prefer_dst)
except pytz.exceptions.NonExistentTimeError:
# Non-existent time (spring forward) - normalize forward
return tz.normalize(tz.localize(dt, is_dst=False))
# Usage
ny_tz = pytz.timezone('America/New_York')
# Missing hour
missing = safe_localize(ny_tz, datetime(2024, 3, 10, 2, 30))
print(missing) # 2024-03-10 03:30:00 EDT (adjusted forward)
# Duplicate hour
duplicate = safe_localize(ny_tz, datetime(2024, 11, 3, 1, 30), prefer_dst=True)
print(duplicate) # 2024-11-03 01:30:00 EDT (first occurrence)
4. 타임스탬프를 UTC로 저장
타임스탬프는 항상 UTC로 저장하고 표시용으로만 현지 시간으로 변환하세요.
// Database storage pattern
class EventScheduler {
// Store in UTC
scheduleEvent(localDateString, timezone) {
const localDate = new Date(localDateString);
const utcTimestamp = localDate.getTime();
// Save to database
return {
utc_timestamp: utcTimestamp,
utc_iso: new Date(utcTimestamp).toISOString(),
original_timezone: timezone
};
}
// Retrieve and display in local time
getEventInTimezone(utcTimestamp, timezone) {
const date = new Date(utcTimestamp);
return date.toLocaleString('en-US', {
timeZone: timezone,
dateStyle: 'full',
timeStyle: 'long'
});
}
}
const scheduler = new EventScheduler();
// Schedule event
const event = scheduler.scheduleEvent('2024-03-10T02:30:00', 'America/New_York');
console.log(event);
// { utc_timestamp: 1710054600000, utc_iso: '2024-03-10T07:30:00.000Z', ... }
// Display in different timezones
console.log(scheduler.getEventInTimezone(event.utc_timestamp, 'America/New_York'));
console.log(scheduler.getEventInTimezone(event.utc_timestamp, 'Europe/London'));
5. DST 엣지 케이스 테스트
항상 DST 전환 날짜를 사용하여 코드를 테스트하세요.
// Test suite for DST handling
describe('DST Transition Tests', () => {
const dstDates = {
springForward: '2024-03-10',
fallBack: '2024-11-03',
missingHour: '2024-03-10T02:30:00',
duplicateHour: '2024-11-03T01:30:00'
};
test('handles missing hour in spring forward', () => {
const result = normalizeToValidTime(
dstDates.missingHour,
'America/New_York'
);
expect(result.wasAdjusted).toBe(true);
});
test('duration calculation on spring forward day', () => {
const start = new Date(`${dstDates.springForward}T00:00:00`);
const end = new Date(`${dstDates.springForward}T23:59:59`);
const hours = (end - start) / 3600000;
expect(hours).toBeCloseTo(23, 0); // 23 hours, not 24
});
test('duration calculation on fall back day', () => {
const start = new Date(`${dstDates.fallBack}T00:00:00`);
const end = new Date(`${dstDates.fallBack}T23:59:59`);
const hours = (end - start) / 3600000;
expect(hours).toBeCloseTo(24, 0); // Appears as 24 but day is 25 hours
});
});
실제 시나리오
시나리오 1: 반복 이벤트 예약
from datetime import datetime, timedelta
import pytz
def schedule_daily_task(start_date, local_time, timezone_name, days=30):
"""
Schedule a task at the same local time each day, accounting for DST.
"""
tz = pytz.timezone(timezone_name)
events = []
for day in range(days):
# Create naive datetime for each day
date = start_date + timedelta(days=day)
naive_dt = datetime.combine(date, local_time)
# Safely localize (handles DST transitions)
try:
localized = tz.localize(naive_dt, is_dst=None)
except pytz.exceptions.NonExistentTimeError:
# Time doesn't exist (spring forward), adjust forward
localized = tz.normalize(tz.localize(naive_dt, is_dst=False))
except pytz.exceptions.AmbiguousTimeError:
# Time occurs twice (fall back), use first occurrence
localized = tz.localize(naive_dt, is_dst=True)
events.append({
'local_time': localized.strftime('%Y-%m-%d %H:%M:%S %Z'),
'utc_time': localized.astimezone(pytz.UTC).strftime('%Y-%m-%d %H:%M:%S UTC'),
'timestamp': int(localized.timestamp())
})
return events
# Schedule daily task at 2:00 AM, crossing DST boundary
from datetime import time, date
events = schedule_daily_task(
start_date=date(2024, 3, 8),
local_time=time(2, 0, 0),
timezone_name='America/New_York',
days=5
)
for event in events:
print(f"{event['local_time']} → {event['utc_time']}")
# Output shows how UTC time shifts on DST transition day
시나리오 2: DST에 따른 업무 시간 계산
function calculateBusinessHours(startDate, endDate, timezone) {
const businessHoursPerDay = 8; // 9 AM - 5 PM
const businessStart = 9;
const businessEnd = 17;
let totalHours = 0;
let currentDate = new Date(startDate);
while (currentDate <= endDate) {
const dayOfWeek = currentDate.getDay();
// Skip weekends
if (dayOfWeek !== 0 && dayOfWeek !== 6) {
// Check if it's a DST transition day
const dayStart = new Date(currentDate);
dayStart.setHours(0, 0, 0, 0);
const dayEnd = new Date(currentDate);
dayEnd.setHours(23, 59, 59, 999);
const dayLength = (dayEnd - dayStart) / 3600000;
if (dayLength < 24) {
// Spring forward - missing hour might affect business hours
console.log(`Spring forward on ${currentDate.toDateString()}`);
totalHours += Math.min(businessHoursPerDay, dayLength - (24 - dayLength));
} else if (dayLength > 24) {
// Fall back - extra hour
console.log(`Fall back on ${currentDate.toDateString()}`);
totalHours += businessHoursPerDay; // Business hours unchanged
} else {
totalHours += businessHoursPerDay;
}
}
currentDate.setDate(currentDate.getDate() + 1);
}
return totalHours;
}
// Calculate business hours March 1-15, 2024 (includes DST transition)
const hours = calculateBusinessHours(
new Date('2024-03-01'),
new Date('2024-03-15'),
'America/New_York'
);
console.log(`Total business hours: ${hours}`);
일반적인 함정
❌ 이러지 마세요
// Bad: Assuming all days are 24 hours
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
const hours = (tomorrow - today) / 3600000; // Won't be exactly 24 on DST days!
// Bad: Using string concatenation for times
const timeString = `${year}-${month}-${day} 02:30:00`;
const date = new Date(timeString); // Might not exist on spring forward day!
// Bad: Ignoring timezone in calculations
const event1 = new Date('2024-11-03T01:30:00'); // Which occurrence?
✅ 대신 이렇게 하세요
// Good: Use UTC for calculations
const tomorrow = new Date(today.getTime() + 86400000); // Always exactly 24 hours
// Good: Use timezone-aware libraries
import { zonedTimeToUtc } from 'date-fns-tz';
const safeDate = zonedTimeToUtc('2024-03-10 02:30', 'America/New_York');
// Good: Always include timezone offset
const event1 = new Date('2024-11-03T01:30:00-04:00'); // First occurrence (EDT)
const event2 = new Date('2024-11-03T01:30:00-05:00'); // Second occurrence (EST)
요약 및 모범 사례
주요 내용
- UTC로 저장 - 타임스탬프는 항상 UTC로 저장하고 표시용으로만 로컬로 변환합니다.
- 시간대 인식 라이브러리 사용 - DST를 수동으로 처리하려고 하지 마세요.
- 특정 사례 검증 - 스프링 포워드 및 폴백 날짜를 사용하여 테스트
- 모호함을 명시적으로 처리 - 중복된 영업시간이 무엇인지 지정하세요.
- 누락된 시간 정규화 - 존재하지 않는 시간을 앞으로 조정하거나 가장 가까운 유효 시간을 사용합니다.
추천 라이브러리
자바스크립트:
date-fns-tz- date-fns에 대한 시간대 지원luxon- 탁월한 DST 처리 기능을 갖춘 최신 날짜/시간 라이브러리moment-timezone- 포괄적인 시간대 데이터베이스(유지 관리 모드)
파이썬:
pytz- Python용 표준 시간대 라이브러리dateutil- 우수한 DST 지원을 갖춘 대안zoneinfo(Python 3.9+) - 내장 시간대 지원
빠른 참조
| 문제 | 솔루션 |
|---|---|
| 누락된 시간(봄 앞으로) | 다음 유효 시간으로 정규화(1시간 앞으로 이동) |
| 시간 중복(대체) | 시간대 오프셋을 사용하여 발생 항목 지정 |
| 기간 계산 | 현지 시간이 아닌 UTC 타임스탬프 사용 |
| 반복되는 이벤트 | 각 항목을 개별적으로 현지화 |
| 테스트 | 항상 DST 전환 날짜로 테스트 |