JS 와 Python 에서 시간을 다룰 때 주의할 점

Einere·2022년 12월 24일
2

시간 포멧 맞추기

FE 작업을 하면서, API 응답 값을 보다가 의문이 들었다.

created_at: "2022-04-18T18:36:31.680000" // ISO 스트링? 근데 왜 zulu 기호가 없지..?
end_date: "2023-11-01"
release_date: "2021-11-02"

시간에 관한 데이터의 포멧이 달랐다. 사실 더 치명적인 점은, 모든 속성이 타임존에 대한 정보가 없어, 추후 확장할 때 문제가 발생할 여지가 많았다.

그래서 나는 BE와 데이터를 다루는 방법 및 포멧을 협의하기 위해 BE 개발자분과 얘기를 나누었다.

얘기를 하다가, A님이 나에게 의문을 제기하셨다.

타임존

아 근데 timestamp로 보내면 tz정보가 없어서 일반적인 js 라이브러리에서는 localtime으로 자동변환해버려요. 그래서 timestamp보다는 tz정보 있는 isostring이 낫긴할거에요. B(사내 FE 서비스)도 그래서 좀 시간추가로 더하고 빼는게 들어간게 있을거에요. js라이브러리에서 timestamp와 tz를 동시에 받는게 있나요?

A님이 슬랙 스레드에 이런 댓글을 주셨다. 그래서 나는 현재 프로젝트에서 사용하고 있는 dayjs 에 대해 알려드렸다. (사실 timestamp 에 tz 정보가 없다는 말에서 빨리 눈치를 챘어야 했는데.. 😨)

A님의 의문

A님이 KST 기준 2022.04.27. 20:30 즈음 뽑은 UNIX timestamp 값

[A님이 KST 기준 2022.04.27. 20:30 즈음 뽑은 UNIX timestamp 값]

A님이 위 스샷을 올리면서, UNIX timestamp 는 타임존 관련 이슈가 있을 것이라는 의견을 주셨다.

그래서 내가 크롬 콘솔창에서 테스트를 해봤다.

테스트 결과

[테스트 결과. KST 기준 11시, DMT 기준 2시인 것으로 확인된다.]

이것을 보고, 나는 A님이 뽑은 UNIX timestamp 값 자체(1651026639*1000)가 당시 시간(2022.04.27. 20:30, KST)보다 9시간 빠른 값이 아닐까 의심했다.

Date 의 동작 방식

아무리 생각해봐도 좀 의심스러워, 좀 공부를 해봤다.

Date 객체의 중심을 구성하는 시간 값은 UTC 기준이지만, 날짜와 시간 등 구성 요소를 가져오는 메서드는 모두 현지(호스트 시스템의 위치)의 시간대를 사용한다는 것을 기억해야 합니다.

MDN Date 문서의 내용이다. MDN 문서를 보고 여러가지 테스트 결과, MDN의 설명이 맞으며, 추가로 아래와 같이 동작한 다는 것을 알게 되었다.

💡 UNIX timestamp는 항상 GMT(UTC) 기준이다!

  1. Date 인스턴스는 내부적으로 UNIX timestamp 로 상태를 가진다.
  2. Date 생성자를 통해 인스턴스를 만들 때, 인자로 받은 값이
    1. 없다면, 로컬 타임존의 보정을 받은 값으로 간주한다.
    2. UNIX timestamp 인 경우, 그 값을 그대로 사용한다.
    3. ISO string 인 경우, 값에 명시된 타임존의 보정을 받은 값으로 간주한다.
    4. b를 제외한 모든 경우, 타임존 오프셋을 뺀 값을 상태로 가진다.
  3. Date 인스턴스는
    1. 일반 getter 함수에 대해, 로컬 타임존 보정을 적용하여 반환한다. (즉, 타임존 오프셋 만큼 더한 후 뱉는다.)
    2. UTC getter 함수에 대해, 로컬 타임존 보정을 적용하지 않고 반환한다.

💡 타임존은 "시간을 해석하는 방법"으로 이해하면 쉽다. 예를 들어, 65 라는 값을 문자로 해석하면 A 가 되지만 정수로 해석하면 65 가 되듯이, 타임스탬프를 어떻게 해석하느냐에 따라 다른 의미를 가지게 된다.

dayjs

그렇다면 내가 자주 쓰는 dayjs 는 어떨까? 코드 샌드박스로 실험한 결과는 다음과 같다.

  1. 내부적으로 UNIX timestamp 로 상태를 가진다.
  2. dayjs 로 생성하던, dayjs.utc 로 생성하던 항상 타임존 보정을 적용한다.
  3. 단, 인자가
    1. 없다면, 로컬 타임존의 보정을 받은 값으로 간주한다.
    2. UNIX timestamp 인 경우, 그 값을 그대로 사용한다.
    3. ISO string인 경우, 값에 명시된 타임존의 보정을 적용한다.
    4. b를 제외한 모든 경우, 타임존 오프셋을 뺀 값을 상태로 설정한다.
  4. dayjs 로 생성했던, dayjs.utc 로 생성했던, toString 함수에 대해서는 항상 GMT 시간을 뱉는다.
  5. format 함수에 대해,
    1. dayjs 로 생성한 인스턴스는 로컬 타임존 보정을 적용해서 뱉는다.
    2. dayjs.utc 로 생성한 인스턴스는 GMT 기준으로 뱉는다.

왜 이런 일이 발생했을까?

결론만 말하자면, 서버에서 클라이언트로 넘어오는 시간문자열에 타임존 정보가 없어서 발생한 것이다.

Python 에서 datetime 객체는 자체적으로 타임존 정보(tzinfo)를 가지고 있다. 여기서 주의해야 할 점이, 나이브 객체와 어웨어 객체가 존재한다는 점이다.

🚨 타임존 정보 포함 유무에 따라, 나이브(naive) 객체와 어웨어(aware) 객체로 구분할 수 있다. 이에 따라, 시간을 처리하거나 시간 문자열을 추출할 때 조심해야 한다.

예를 들어, 한국시로 "1970년 1월 1일 0시 0분 0초" 라는 값을 서버에서 클라이언트로 내려주는 상황을 가정해보자.
만약 BE 개발자가 tzinfo 에 대해 잘 모른다면(혹은 신경쓰지 않는다면) 다음과 같이 코딩할 것이다. (서버 위치는 편의상 한국이라고 가정하자.)

# python
>>> d = datetime(1970, 1, 1) # BE 개발자가 tzinfo 설정하는 것을 까먹었다면..?
>>> d.tzinfo == None
True 
>>> d.isoformat() # 타임존 정보가 없으므로, 현지 시각으로 해석한다.
                  # 즉, 한국 시간으로 70년 1월 1일 0시 0분 0초를 의미.
'1970-01-01T00:00:00' # Z가 붙지 않은 것에 주의!

이제 '1970-01-01T00:00:00' 를 클라이언트에 내려주면 FE 개발자는 이를 Date 클래스를 이용해 다룰 것이다.
운이 좋아 만약 유저가 한국에 살고 있다면 다음과 같이 올바른 값을 본다.

// javascript
> const d = new Date('1970-01-01T00:00:00');
> d.toString();
'Thu Jan 01 1970 00:00:00 GMT+0900 (한국 표준시)' // 다행히 한국시로 1970년 1월 1일 0시가 나온다.

그런데 만약 영국 런던에 사는 유저라면?

> const d = new Date('1970-01-01T00:00:00')
> d.toString();
'Thu Jan 01 1970 00:00:00 GMT+0100 (그리니치 표준시)' // 분명 한국 기준으로 1970년 1월 1일 0시 0분 0초를 보내주었는데..? 

영국에 사는 사람은 9시간을 뺀 값인 "1969년 12월 31일 15시"로 나와야 하는데 한국 유저가 보는 것과 동일한 1970년 1월 1일 0시로 보이게 된다.

이는 클라이언트에서 받는 시간 문자열에 타임존 정보가 없기 때문이다.

만약 서버에서 타임존 정보를 포함한 시간 문자열을 내려준다고 가정해보자.

>>> d1 = datetime(1970, 1, 1, tzinfo=timezone(timedelta(hours=9)))
>>> d1.isoformat()
'1970-01-01T00:00:00+09:00'
// 한국 유저
> const d = new Date('1970-01-01T00:00:00+09:00')
> d.toString();
'Thu Jan 01 1970 00:00:00 GMT+0900 (한국 표준시)'

// 영국 유저
> const d = new Date('1970-01-01T00:00:00+09:00')
> d.toString();
'Wed Dec 31 1969 16:00:00 GMT+0100 (그리니치 표준시)' // 1시간의 차이가 있네요.. 😓

이제 한국 유저든 영국 유저든, 세계 어디서도 동일한 시간을 보게 됩니다.

이제 실제로 제가 겪은 사례를 보자면,

  1. KST 기준 2022.04.27. 20:30 에 대한 나이브 데이트 객체를 만들고
  2. 네이브 데이트 객체에서 시간 문자열을 얻을 때, 타임존 정보가 없는 시간 문자열을 얻는다. (KST 기준 2022.04.27. 11:30)
  3. 프론트에서 타임존 정보가 없는 시간 문자열 값을 받아 Date 객체를 만들면, 이 값을 현지 시간대로 해석하여, 내부적으로는 KST 보정 값(+9H)를 뺀다. (KST 기준 2022.04.27. 02:30)
  4. 이 객체에서 UTC getter 이외의 메소드로 결과 값을 추출할 때, KST 보정 값(+9H)를 더해서 뱉는다. (KST 기준 2022.04.27. 11:30)

타임존 정보가 없는 "시간 문자열" 때문에 여러명이 고통받을 수 있다. 😵 이런 것을 일일이 신경쓰기 귀찮다면 UTC 기준인 unix timestamp 로 통일해서 사용하자!

참고

왜 내가 작성한 JavaScript Date 코드가 서버에서는 다르게 보이는 거죠?

profile
지속가능한 웹 개발자를 지향합니다. 경험의 공유를 통해 타인에게 도움이 되는 것을 좋아합니다. 사용자에게 가치를 제공하는 것에 기쁨을 느낍니다.

0개의 댓글