자바스크립트의 타임존 지원은 다른 언어들에 비해서 부실하다는 이야기가 있다. 사실 크게 와닿지는 않는다. 글로벌 서비스를 진행해본적도 없고.. 생각도 안하고있었기 때문...
Timezone이란?
타임존은 동일한 로컬 시간을 따르는 지역(우리나라 같은 경우는 서울이나 부산이나 시간이 똑같다.)을 의미하며, 주로 해당 국가에 의해 법적으로 지정된다. 보통 국가별로 각자의 고유한 타임존을 사용하고 있으며, 미국처럼 면적이 넓은 나라의 경우 지역별로 다른 타임존을 사용하기도 한다.
GMT(Greenwich Mean Time)
한국의 타임존은 보통 GMT+09:00 으로 표현한다. 경도 0도에 위치한 영국의 그리니치 천문대를 기준으로 하는 태양 시간을 의미한다. GMT 시간은 1925년 2월 5일부터 사용하기 시작하였으며, 1972년 1월 1일까지 세계 표준시로 사용되었다.
UTC(Universal Time Coordinated)
1972년 1월 1일부터 시행된 국제 표준시로 1970년 1월 1일 자정을 0 밀리초로 설정하여 기준을 삼아 그 후로 시간의 흐름을 밀리초로 계산한다.
GMT와 UTC는 혼영되어 사용되고 있기는 하지만 엄밀히 구분하면 둘은 다른 의미를 가진다. UTC는 지구 자전주기의 흐름이 늦어지고 있는 문제를 해결하기 위해 1972년에 세슘 원자의 진동수에 기반한 국제 원자시를 기준으로 다시 지정된 시간대이다.
오프셋
UTC+09:00의 +09:00의 의미는 UTC 기준시간보다 9시간이 빠르다는 의미이다.
우리나라의 타임존은 KST(Korea Standard Time)이라고 부르는데 KST = UTC+09:00이라고 보면된다. 하지만 UTC+09:00 = KST라고 볼 수 없다. 한국 뿐만아니라 일본, 인도네시아 등 여러 지역에서 사용하고 있기 때문이다.
타임존 !== 오프셋
타임존을 말할 때는 보통 KST, JST 등과 같이 오프셋과 동일한 의미로 쓰인다. 하지만, 특정 지역의 타임존을 단순히 오프셋이라고 지칭하기는 어렵다. 특정 지역의 타임존을 단순히 오프셋이라고 지칭하기 어려운 이유는 아래 두 가지 때문이다.
서머 타임(DST)
국내에서는 생소한 개념이지만, 해외 여러 국가에서는 서머타임이 존재한다. 사실 서머 타임은 주로 영국이나 유럽에서 쓰이는 용어인데, 좀더 범용적인 용어로는 일광 시간 절약제(DST, Daylight Saving Time)라고 불린다. 이는 하절기에 표준시를 원래 시간보다 한 시간 앞당긴 시간으로 이용하는 것을 의미한다.
타임존은 변한다?
어떤 타임존을 이용할 지는 해당 지역 혹은 국가가 법적으로 결정하기 때문에, 정치적 혹은 경제적 이유로 변경될 수 있다.
서버 - 클라이언트 환경에서의 타임존
예를들어 간단한 일정 관리 프로그램을 생각해보자. 먼저, 클라이언트 환경에서 사용자가 등록 페이지에서 입력 박스에 일정(날짜 및 시간)을 입력하면, 그 정보를 서버로 전송해서 DB에 저장된다. 그리고 목록 페이지에서는 클라이언트가 다시 서버로부터 등록된 일정의 정보를 받아와서 화면에 보여주게 된다.
이때, 서버에 저장된 동일한 데이터에 접근하는 클라이언트들이 서로 다른 타임존을 갖고 있을 수 있다는 점이다. 즉, 서울에서 2023년 1월 26일 오전 11시 30분이라는 일정을 등록하고, 뉴욕에서 해당 일정을 조회한다면 2023년 1월 26일 오후 9시 30분이라고 표시되어야 한다. 이런식으로 다양한 타임존의 클라이언트 환경을 지원하기 위해서는 서버에 저장되는 데이터가 타임존에 영향을 받지 않는 절대값이어야 한다. 서버에서 이러한 절대값을 어떤 데이터 형태로 저장하는지는 각 서버나 데이터베이스 환경에 따라 다를 것이다.
일반적으로 이런 데이터는 UTC를 기준으로 한 유닉스 시간(1674736200)이나 오프셋 정보가 포함된 ISO-8601(Thu Jan 26 2023 21:30:00 UTC+0900 (한국 표준시))와 같은 형태로 전송하게 된다.
브라우저 환경에서 자바스크립트를 이용해 이러한 처리를 해야 한다면, 우리는 사용자의 입력 값을 위와 같은 형식으로 변환하는 작업과, 위와 같은 형식의 데이터를 받아서 사용자의 타임존에 맞게 변환하는 작업 두 가지 모두 고려해야 한다. 흔히 사용하는 용어로 표현하자면 앞의 작업은 파싱(Parsing), 그리고 뒤의 작업은 포맷팅(Formatting)이라고 할 수 있다. 그렇다면 자바스크립트에서 이들을 어떻게 처리할까?
자바스크립트의 Date 객체
자바스크립트에서 날짜, 시간 관련된 모든 작업은 Date 객체를 이용해서 처리한다. Array나 Function과 같이 ECMAScript 스펙에 정의되어 있는 네이티브 객체이며, 주로 C++과 같은 네이티브로 구현된다. API는 MDN문서에 잘 정리되어 있는데, java.util.Date 클래스에서 영향을 많이 받았다고 한다. 그래서 불변 데이터가 아니라는 점이나, Month가 0으로 시작하는 등의 안좋은 특징까지 같이 공유하고 있다.
자바스크립트의 Date 객체는 내부적으로 유닉스 시간과 같은 절대값으로 시간 데이터를 관리한다. 하지만 생성자나 parse()함수, getHour(), setHour() 등의 메소드들은 모두 클라이언트의 로컬 타임존(브라우저가 실행되는 운영체제에 설정된 타임존)에 영향을 받는다. 그러므로 사용자가 입력한 데이터를 잉요해 그대로 Date객체를 생성하거나 값을 지정한다면 그 데이터는 클라이언트의 로컬 타임존을 그대로 반영하게 될 것이다.
사용자의 입력값을 이용한 Date 객체 생성
let d1 = new Date(2023, 0, 27, 11, 30);
console.log(d1); // 2023-01-27T02:30:00.000Z
console.log(d1.toString(d1)); //Fri Jan 27 2023 11:30:00 GMT+0900 (대한민국 표준시)
Date객체에 년도, 월, 일, 시, 분 단위 순서로 저장하면, 2023년 01월 27일 11시 30분을 의미한다. (월은 0부터)
Date객체의 생성자에 문자열 타입의 값을 사용하면, 내부적으로 Date.parse() 를 호출하여 적절한 값을 계산해내며, 이 함수는 RFC2888 스펙과 ISO-8601 스펙을 지원한다. 하지만 MDN의 Date.parse() 문서에도 나와있듯이, 이 메소드의 결과값은 브라우저마자 구현상태가 다르고, 문자열의 형태에 따라 정확한 값을 예측하기 어렵기 때문에 사용하지 않기를 권장하고 있다.
서버 데이터를 이용한 Date 객체 생성
서버로부터 데이터를 전달받는 경우, 만약 데이터가 숫자형의 유닉스 시간값이라면, 간단하게 생성자를 이용해 Date 객체를 생성할 수 있다. Date 생성자는 인자로 숫자 하나만을 받으면 년도 값이 아닌 밀리초 단위의 유닉스 시간으로 인식한다.
let d2 = new Date(1674786600000);
console.log(d2); // 2023-01-27T02:30:00.000Z
console.log(d2.toString()); // Fri Jan 27 2023 11:30:00 GMT+0900 (대한민국 표준시)
유닉스 시간이 아닌 ISO-8601과 같은 문자열 타입은 위에서 설명했듯이 Date.parse() 메소드는 결과를 신뢰할 수 없기 때문에 사용하지 않는 것을 권장하고 있다. 하지만 ECMAScript5부터는 ISO-8601을 지원하도록 명시되어 있기 때문에, ECMAScript5를 지원하는 브라우저에서는 주의해서 사용하면 ISO-8601 형식의 문자열을 Date 생성자에 사용할 수 있다.
let d3 = new Date("2023-01-27T11:30:00");
let d4 = new Date("2023-01-27T11:30:00Z");
console.log(d3.toString()); // Fri Jan 27 2023 11:30:00 GMT+0900 (대한민국 표준시)
console.log(d4.toString()); // Fri Jan 27 2023 20:30:00 GMT+0900 (대한민국 표준시)
최신 브라우저가 아닌 경우 마지막에 Z문자가 없으면 UTC 기준으로 해석해야 함에도 불구하고 로컬 타임을 기준으로 해석하는 경우가 있다. 스펙에 따르면 두 결과값이 같아야 함에도 불구하고 다른 것을 볼 수 있다. 브라우저 버전별로 다르게 해석되는 문제를 막기 위해서는 타임존 데이터가 없는 경우 문자열의 마지막에 항상 Z를 추가해 주어야 한다.
서버로 전달할 데이터 생성
Date객체를 이용하면 로컬 타임존을 기준으로 날짜나 시간을 더하거나 빼는 등의 연산을 자유롭게 할 수 있다. 하지만 마지막에 다시 서버로 데이터를 전송하기 위해서는 데이터를 변환하는 과정이 필요하다.
유닉스 시간 형식의 경우 getTime() 메소드를 이용해서 간단하게 수행할 수 있다.
let d5 = new Date(2023, 0, 27, 11, 30);
let d6 = new Date("2023-01-27T11:30:00");
let d7 = new Date("2023-01-27T11:30:00Z");
console.log(d5.getTime()); // 1674786600000
console.log(d6.getTime()); // 1674786600000
console.log(d7.getTime()); // 1674819000000
ECMAScript5 이상을 지원하는 브라우저는 ISO-8601 형식의 문자열을 지원하며, toISOString(), toJSON을 이용하면 생성할 수 있다.
두 메소드의 결과값은 동일한데, 유효하지 않은 데이터에 대한 처리만 다르다.
let d8 = new Date(2023, 0, 27, 11, 30);
let d9 = new Date("hello");
console.log(d8.toJSON()); // 2023-01-27T02:30:00.000Z
console.log(d8.toISOString()); // 2023-01-27T02:30:00.000Z
console.log(d9.toJSON()); // null
console.log(d9.toISOString()); // Range Error: Invalid time value
로컬 타임존 변경하기
다양한 타임존에서의 시간을 하나의 어플리케이션에서 동시에 보여주어야 한다면, 로컬의 타임존을 직접 변경할 수 없으므로 타임존의 오프셋 값을 알고 있는 경우, 오프셋값을 더하거나 빼서 직접 날짜를 계산해야한다.
이 때 사용할 수 있는 메소드가 getTimeZoneOffset() 메소드이다.
let seoul = new Date(2023, 0, 27, 11, 30);
console.log(seoul.getTimezoneOffset()); // -540
반환값 -540은 타임존이 540분이 앞서 있다는 의미이다. 서울의 오프셋은 +09:00 이란 걸 생각해보면 부호가 반대로 되어있다. 이 방식을 기준으로 뉴욕의 오프셋 -05:00을 계산해 보면 60*5 = 300이 된다. 서울과 뉴욕의 840만큼의 차이를 밀리초 단위로 보정해서 새로운 Date 객체를 만들면, 그 객체의 메소드들을 이용해서 원하는 형테의 데이터를 만들어 낼 수 있다.
function formatDate(date) {
const year = date.getFullYear();
const month = date.getMonth() + 1;
const d = date.getDate();
const hours = date.getHours();
const minutes = date.getMinutes();
return (
year + "년 " + month + "월 " + d + "일 " + hours + "시 " + minutes + "분"
);
}
const seo = new Date(2023, 0, 27, 11, 30);
const ny = new Date(seo.getTime() - 840 * 60 * 1000);
console.log(formatDate(seo)); // 2023년 1월 27일 11시 30분
console.log(formatDate(ny)); // 2023년 1월 26일 21시 30분
formatDate() 의 결과에서 서울과 뉴욕의 시간대에 맞게 날짜가 잘 표시되고 있는 것을 볼 수 있다. 하지만 오프셋만 알고 있다고해서 로컬 타임존을 변경할 수는 없다. 타임존은 단순히 오프셋이 아니라 모든 오프셋 변경의 히스토리를 담고 있는 일종의 데이터베이스이기 때문이다. 정확한 타임존 계산을 위해서는 단순히 현재 시점의 오프셋이 아닌, 해당 날짜가 가르키는 시점의 오프셋을 알아야 한다.
로컬 타임존 변경의 문제
사용자가 뉴욕의 시간대에서 해당 시간을 확인한 후에 날짜를 11일에서 15일로 변경하려고 한다. Date객체의 setDate() 메소드를 이용하면 다른 항목들은 유지한 채로 날짜 값만 변경할 수 있다.
하지만 이 데이터를 다시 서버로 전송해야 한다면 이 데이터 자체를 변경했기 때문에, getTime()이나 getISOString() 등의 메소드를 사용할 수 없다. 그러므로 서버로 전송하기 위해서는 앞에서 했던 계산을 역으로 해서 원래 데이터를 연산해야만 한다.
서울 기준의 Date 객체에서 15일로 변경했다면 11일에서 15일이 되었으므로 4일(24 * 4 * 60 * 60 * 1000)이 추가되고, 뉴욕 기준에서는 10일에서 15일이 되었으므로 5일(24 * 5 * 60 * 60 * 1000)이 추가된 것이다. 즉, 날짜 연산을 할 때에도 해당 오프셋을 기준으로 진행해야만 정확한 연산이 가능하다.
하지만 더 중요한 문제는, 단순히 오프셋을 더하고 빼는 것만으로는 해결되지 않는 문제가 있다는 것이다. 뉴욕의 타임존은 3월 12일부터 서머타임이 적용되기 때문에 2017년 3월 15일은 오프셋이 -05:00이 아닌 -04:00이 되어야 한다. 즉 역으로 연산할 때에는 현재보다 60분이 적은 780분 만큼만 더해주어야 한다.
결론적으로 단순히 전달받은 오프셋 값만 가지고 원하는 타임존 기준으로 날짜 연산을 할 수가 없다. 뿐만 아니라, 서머타임이 적용되는 규칙까지 안다고 해도 여전히 허점이 존재한다. 즉, 정확한 날짜 연산을 위해서는 IANA timezone Database와 같이 타임존 변경 히스토리가 담긴 전체 데이터가 필요한 것이다.
해결을 위해서는 전체 타임존 데이터베이스를 저장하고 있다가, Date 객체에서 날짜나 시간 데이터를 가져올 때마다 데이터베이스에서 해당 날짜와 타임존에 맞는 오프셋을 알아낸 다음, 위와 같은 연산을 통해 결과를 반환해야 한다. 이론적으로는 가능하지만, 이를 위해서는 너무 많은 노력이 필요하고, 실제 변환된 데이터가 정확한지 테스트를 하기도 쉽지 않다.
https://ko.wikipedia.org/wiki/%EC%8B%9C%EA%B0%84%EB%8C%80
https://meetup.nhncloud.com/posts/125
https://ui.toast.com/weekly-pick/ko_20170804
https://ui.toast.com/weekly-pick/ko_20170922