XLOG

Swift에서 시간, 날짜 다루기 본문

Swift

Swift에서 시간, 날짜 다루기

X_PROFIT 2025. 7. 9. 01:08

한번씩 하려다보면 항상 검색을 해야해서 정리해보았다...

1. Date - 기본 날짜/시간 타입

Date는 Swift의 핵심 날짜/시간 타입으로, Unix 타임스탬프(1970년 1월 1일 00:00:00 UTC부터의 초)를 기반으로 한다.

// 현재 시간
let now = Date()

// 특정 시간 생성
let specificDate = Date(timeIntervalSince1970: 1640995200) // 2022-01-01 00:00:00 UTC

// 상대적 시간 생성
let oneHourAgo = Date(timeIntervalSinceNow: -3600)
let tomorrow = Date(timeIntervalSinceNow: 86400)

2. TimeInterval - 시간 간격

TimeIntervalDouble 타입의 별칭으로, 초 단위의 시간 간격을 나타낸다. 마치 두 지점 사이의 거리를 측정하는 자처럼 작동한다.

let interval: TimeInterval = 3600 // 1시간 = 3600초
let start = Date()
let end = Date(timeIntervalSinceNow: 7200) // 2시간 후

// 두 날짜 간의 차이 계산
let difference = end.timeIntervalSince(start) // 7200.0초

3. DateComponents - 날짜 구성 요소

DateComponents는 날짜를 년, 월, 일, 시, 분, 초 등의 개별 구성요소로 분해하거나 조합할 때 사용한다.

var components = DateComponents()
components.year = 2024
components.month = 7
components.day = 9
components.hour = 14
components.minute = 30

// Calendar를 사용해 Date로 변환
let calendar = Calendar.current
let date = calendar.date(from: components)

4. Calendar - 달력 시스템

Calendar는 날짜 계산의 핵심 엔진

let calendar = Calendar.current

// Date를 DateComponents로 분해
let components = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: Date())

// 날짜 계산
let nextWeek = calendar.date(byAdding: .day, value: 7, to: Date())
let startOfDay = calendar.startOfDay(for: Date())

// 두 날짜 사이의 차이
let difference = calendar.dateComponents([.day, .hour], from: Date(), to: nextWeek!)

5. DateFormatter - 문자열 변환

DateFormatter는 Date와 String 사이의 번역사 역할

let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
formatter.locale = Locale(identifier: "ko_KR")
formatter.timeZone = TimeZone(identifier: "Asia/Seoul")

// Date -> String
let dateString = formatter.string(from: Date())

// String -> Date
let dateFromString = formatter.date(from: "2024-07-09 14:30:00")

// 미리 정의된 스타일 사용
formatter.dateStyle = .medium
formatter.timeStyle = .short
let readableString = formatter.string(from: Date()) // "2024. 7. 9. 오후 2:30"

6. ISO 8601 형식 다루기

6.1 ISO8601DateFormatter

let isoFormatter = ISO8601DateFormatter()

// 기본 형식 (UTC 기준)
let basicISO = isoFormatter.string(from: Date())
// "2024-07-09T05:30:00Z"

// 다양한 형식 옵션
isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let preciseISO = isoFormatter.string(from: Date())
// "2024-07-09T14:30:00.123Z"

// 시간대 지정
isoFormatter.timeZone = TimeZone(identifier: "Asia/Seoul")
isoFormatter.formatOptions = [.withInternetDateTime]
let localISO = isoFormatter.string(from: Date())
// "2024-07-09T23:30:00+09:00"

// String -> Date 변환
let dateFromISO = isoFormatter.date(from: "2024-07-09T14:30:00Z")

6.2 DateFormatter로 ISO 8601 다루기

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
dateFormatter.locale = Locale(identifier: "en_US_POSIX") // 필수!
dateFormatter.timeZone = TimeZone(identifier: "UTC")

let isoString = dateFormatter.string(from: Date())
// "2024-07-09T14:30:00+0000"

6.3 ISO8601DateFormatter vs DateFormatter 비교

구분 ISO8601DateFormatter DateFormatter
로케일 안전성 ✅ 자동으로 안전 ⚠️ 수동 설정 필요
표준 준수 ✅ 완벽한 표준 준수 ⚠️ 개발자 실수 가능
파싱 유연성 ✅ 다양한 형식 자동 지원 ❌ 정확한 형식만 파싱
성능 ✅ 최적화된 구현 ⚠️ 설정 오버헤드
커스터마이징 ❌ 제한적 ✅ 자유로운 형식

로케일 안전성 문제

// ❌ 위험한 방식 - 사용자 로케일에 영향받을 수 있음
let unsafeFormatter = DateFormatter()
unsafeFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
// 아랍어나 힌디어 로케일에서 아라비아 숫자 대신 다른 숫자 체계 사용 가능

// ✅ 안전한 DateFormatter 방식
let safeFormatter = DateFormatter()
safeFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
safeFormatter.locale = Locale(identifier: "en_US_POSIX") // 필수!

// ✅ 가장 안전한 방식
let isoFormatter = ISO8601DateFormatter()

파싱 유연성 비교

// DateFormatter는 정확한 형식만 파싱 가능
let strictFormatter = DateFormatter()
strictFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
strictFormatter.locale = Locale(identifier: "en_US_POSIX")

// ❌ 이런 변형들은 파싱 실패
let variations = [
    "2024-07-09T14:30:00Z",      // Z 표기
    "2024-07-09T14:30:00+00:00", // 오프셋 표기
    "2024-07-09T14:30:00.123Z"   // 밀리초 포함
]

// ✅ ISO8601DateFormatter는 다양한 변형 자동 처리
let flexibleISO = ISO8601DateFormatter()
for variation in variations {
    if let date = flexibleISO.date(from: variation) {
        print("파싱 성공: \(date)")
    }
}

성능 및 메모리 효율성

// DateFormatter - 더 많은 설정 필요
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.timeZone = TimeZone(identifier: "UTC")

// ISO8601DateFormatter - 최적화된 내부 구현
let isoFormatter = ISO8601DateFormatter()
isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]

// 성능 테스트 예제
func performanceTest() {
    let dates = (0..<1000).map { _ in Date() }

    // DateFormatter 성능
    let start1 = Date()
    for date in dates {
        let _ = dateFormatter.string(from: date)
    }
    let dateFormatterTime = Date().timeIntervalSince(start1)

    // ISO8601DateFormatter 성능
    let start2 = Date()
    for date in dates {
        let _ = isoFormatter.string(from: date)
    }
    let isoFormatterTime = Date().timeIntervalSince(start2)

    print("DateFormatter: \(dateFormatterTime)s")
    print("ISO8601DateFormatter: \(isoFormatterTime)s")
}

언제 어떤 것을 사용할까?

ISO8601DateFormatter 사용 권장:

  • API 통신
  • 로그 시스템
  • 데이터베이스 저장
  • 국제화된 앱
  • 시스템 간 데이터 교환

DateFormatter 사용 권장:

  • 커스텀 형식이 필요한 경우
  • 사용자 인터페이스 표시
  • 레거시 시스템 호환성
// ✅ ISO8601DateFormatter - API 통신
let apiFormatter = ISO8601DateFormatter()
let apiString = apiFormatter.string(from: Date())

// ✅ DateFormatter - 사용자 표시용
let displayFormatter = DateFormatter()
displayFormatter.dateStyle = .medium
displayFormatter.timeStyle = .short
displayFormatter.locale = Locale.current
let userString = displayFormatter.string(from: Date())

7. 실용적인 확장 예제

extension Date {
    // 사용자 친화적인 상대 시간
    var timeAgoDisplay: String {
        let formatter = RelativeDateTimeFormatter()
        formatter.unitsStyle = .full
        return formatter.localizedString(for: self, relativeTo: Date())
    }

    // ISO 8601 문자열 (권장 방식)
    var iso8601String: String {
        let formatter = ISO8601DateFormatter()
        formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
        return formatter.string(from: self)
    }

    // 특정 시간대의 문자열 반환
    func string(format: String, timeZone: TimeZone = .current) -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = format
        formatter.timeZone = timeZone
        formatter.locale = Locale(identifier: "en_US_POSIX")
        return formatter.string(from: self)
    }

    // 하루의 시작/끝
    var startOfDay: Date {
        Calendar.current.startOfDay(for: self)
    }

    var endOfDay: Date {
        Calendar.current.date(bySettingHour: 23, minute: 59, second: 59, of: self) ?? self
    }
}

extension TimeInterval {
    // TimeInterval을 읽기 쉬운 형태로
    var readableTime: String {
        let hours = Int(self) / 3600
        let minutes = Int(self) % 3600 / 60
        let seconds = Int(self) % 60

        if hours > 0 {
            return String(format: "%d:%02d:%02d", hours, minutes, seconds)
        } else {
            return String(format: "%02d:%02d", minutes, seconds)
        }
    }
}

8. 성능 최적화

Formatter들은 생성 비용이 높으므로 재사용하는 것이 좋습니다:

https://velog.io/@qwerty3345/Swift-DateFormatter는-당신의-생각보다-비싸다

핵심 원리

  1. Date: 절대적인 시점 (Unix 타임스탬프 기반)
  2. Calendar: 달력 시스템과 계산 엔진
  3. DateComponents: 인간이 이해하는 날짜 구성요소
  4. DateFormatter/ISO8601DateFormatter: 표현 계층 (문자열 변환)

ISO 8601 처리에서는 ISO8601DateFormatter를 우선적으로 사용하되, 특별한 커스터마이징이 필요한 경우에만 DateFormatter를 신중하게 사용하는 것이 좋습니다. 또한 함수 내부에서 DateFormatter를 생성 후 변환을 하게 되면 매번 Formatter 생성 비용이 발생하므로, 날짜를 자주 다룬다면 전역적으로 관리하는 것에 대한 고민을 해보는 것이 좋습니다.