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님이 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) 기준이다!
Date
인스턴스는 내부적으로 UNIX timestamp 로 상태를 가진다.Date
생성자를 통해 인스턴스를 만들 때, 인자로 받은 값이Date
인스턴스는💡 타임존은 "시간을 해석하는 방법"으로 이해하면 쉽다. 예를 들어,
65
라는 값을 문자로 해석하면A
가 되지만 정수로 해석하면65
가 되듯이, 타임스탬프를 어떻게 해석하느냐에 따라 다른 의미를 가지게 된다.
dayjs
그렇다면 내가 자주 쓰는 dayjs
는 어떨까? 코드 샌드박스로 실험한 결과는 다음과 같다.
dayjs
로 생성하던, dayjs.utc
로 생성하던 항상 타임존 보정을 적용한다.dayjs
로 생성했던, dayjs.utc
로 생성했던, toString
함수에 대해서는 항상 GMT 시간을 뱉는다.format
함수에 대해, dayjs
로 생성한 인스턴스는 로컬 타임존 보정을 적용해서 뱉는다.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시간의 차이가 있네요.. 😓
이제 한국 유저든 영국 유저든, 세계 어디서도 동일한 시간을 보게 됩니다.
이제 실제로 제가 겪은 사례를 보자면,
타임존 정보가 없는 "시간 문자열" 때문에 여러명이 고통받을 수 있다. 😵 이런 것을 일일이 신경쓰기 귀찮다면 UTC 기준인 unix timestamp 로 통일해서 사용하자!