자바스크립트

자바스크립트에서 타임존 다루기 (2)

NHNEnt의 기술블로그인 Toast Meetup에 연재된 ‘자바스크립트에서 타임존 다루기’의 두 번째 글이다. 1부에서는 타임존과 오프셋에 대한 개념과 IANA timezone Database 등에 대해 자세히 설명하고 있다. 이 글은 1부에서 다룬 내용을 바탕으로 자바스크립트에서의 타임존에 대해 설명하고 있으므로, 1부를 읽지 않은 분들은 꼭 먼저 읽어보길 권한다.

1부에서 자바스크립트의 타임존 지원은 다른 언어에 비해 상당히 부족한 편이라고 이야기했다. 하지만 미약하나마 자바스크립트에서도 타임존을 다룰 수 있는 방법을 제공하고는 있다. 2부에서는 자바스크립트에서 지원하는 타임존 관련 API들과 이들의 한계에 대해 좀더 자세히 알아보고, 이를 보완할 수 있는 방법을 찾아보도록 하겠다.

서버 – 클라이언트 환경에서의 타임존

먼저 타임존을 고려해야 하는 간단한 시나리오를 생각해보자. 시간 정보를 다루어야 할테니 간단한 일정 관리 프로그램이 어울릴 것 같다. 먼저, 클라이언트 환경에서 사용자가 등록 페이지에서 입력박스에 일정(날짜 및 시간) 입력하면, 그 정보를 서버로 전송해서 DB에 저장된다. 그리고 목록 페이지에서는 클라이언트가 다시 서버로부터 등록된 일정의 정보를 받아와서 화면에 보여주게 된다.

이때 유의할 점은, 서버에 저장된 동일한 데이터에 접근하는 클라이언트들이 서로 다른 타임존을 갖고 있을 수 있다는 점이다. 즉, 서울에서 2017년 3월 10일 오전 11시 30분이라는 일정을 등록하고, 뉴욕에서 해당 일정을 조회한다면 2017년 3월 10일 오후 9시 30분이라고 표시되어야 한다. 이런식으로 다양한 타임존의 클라이언트 환경을 지원하기 위해서는 서버에 저장되는 데이터가 타임존에 영향을 받지 않는 절대값이어야 한다. 서버에서 이러한 절대값을 어떤 데이터 형태로 저장하는지는 각 서버나 데이터베이스 환경에 따라 다를 것이며, 이 글의 주제를 벗어나는 내용이므로 깊게 다루지 않겠다. 다만, 이를 위해서는 클라이언트에서 서버에 전달하는 날짜 및 시간 정보가 동일한 오프셋(보통 UTC)에 맞추어진 값이거나, 해당 클라이언트 환경의 타임존 정보까지 포함된 값이어야 할 것이다.

일반적으로 이런 데이터는 UTC를 기준으로 한 유닉스 시간이나 오프셋 정보가 포함된 ISO-8601와 같은 형태로 전송하게 된다. 위의 예시에서 서울의 2017년 3월 10일 오후 9시 30분은 유닉스 시간을 이용한다면 숫자 타입의 1489113000 이 될 것이고, ISO-8601을 이용한다면 문자열 타입의 2017-03-10T11:30:00+09:00 가 될 것이다.

브라우저 환경에서 자바스크립트를 이용해 이러한 처리를 해야 한다면, 우리는 사용자의 입력 값을 위와 같은 형식으로 변환하는 작업과, 위와 같은 형식의 데이터를 받아서 사용자의 타임존에 맞게 변환하는 작업 두 가지를 모두 고려해야 한다. 흔히 사용하는 용어로 표현하자면 앞의 작업은 파싱(Parsing), 그리고 뒤의 작업은 포맷팅(Formatting)이라고 할 수 있을 것이다. 그러면, 자바스크립트에서 이들을 어떻게 처리하는지 하나씩 살펴보도록 하자.

Node.js를 이용한 서버 환경에서 자바스크립트를 사용할 때에도 경우에 따라 클라이언트로부터 전달받은 데이터를 파싱해야 하는 등의 작업이 필요할 때가 있다. 하지만, 보통 서버 환경의 타임존은 데이터베이스와 동일하게 설정되어 있고, 포맷팅을 주로 클라이언트에게 위임하는 경우가 많기 때문에 브라우저 환경보다는 고려할 요소가 적을 것이다. 이 글에서는 브라우저 환경을 중심으로 설명하도록 하겠다.

자바스크립트의 Date 객체

자바스크립트에서 날짜나 시간과 관련된 모든 작업은 Date 객체를 이용해서 처리한다. Array나 Function과 같이 ECMAScript 스펙에 정의되어 있는 네이티브 객체이며, 주로 C++과 같은 네이티브 코드로 구현된다. API는 MDN 문서에 잘 정리되어 있는데, Java의 java.util.Date 클래스에서 영향을 많이 받았다고 한다. 그래서 불변(Immutable) 데이터가 아니라는 점이나, Month가 0으로 시작하는 등의 안좋은 특징까지 같이 공유하고 있다.

자바스크립트의 Date 객체는 내부적으로 유닉스 시간과 같은 절대값으로 시간 데이터를 관리한다. 하지만 생성자나 parse() 함수, getHour(), setHour() 등의 메소드들은 모두 클라이언트의 로컬 타임존 (정확히는 브라우저가 실행되는 운영체제에 설정된 타임존)에 영향을 받는다. 그러므로 사용자가 입력한 데이터를 이용해 그대로 Date 객체를 생성하거나 값을 지정한다면 그 데이터는 클라이언트의 로컬 타임존을 그대로 반영하게 될 것이다.

1부에서 잠깐 언급했듯이, 자바스크립트는 임의로 타임존을 변경할 수 있는 방법을 제공하지 않는다. 그러므로, 일단 브라우저의 타임존 설정을 그대로 반영해도 되는 상황이라고 가정하고 설명을 이어가도록 하겠다.

사용자의 입력값을 이용한 Date 객체 생성

처음의 예시를 계속 이어가 보자. 사용자가 타임존이 서울로 설정된 기기에서 2017년 3월 11일 오전 11시 30분을 입력했다. 이 입력값을 년도, 월, 일, 시, 분 단위로 각각 숫자 형태의 2017, 2, 11, 11, 30 로 저장했다고 가정해보자. (월은 0부터 시작하기 때문에 3에서 1을 뺀 2가 되어야 한다). 생성자를 이용하면, 각 단위별 숫자값을 이용해서 간단하게 Date 객체를 생성할 수 있다.

var d1 = new Date(2017, 2, 11, 11, 30);
d1.toString(); // Sat Mar 11 2017 11:30:00 GMT+0900 (KST)

d1.toString() 이 반환하는 값을 보면 생성된 객체의 절대값은 오프셋 +09:00 (KST) 기준의 2017년 3월 11일 11시 30분인 걸 알 수 있다.

생성자를 이용하는 또 하나의 방법은 문자열 데이터를 이용하는 것이다. Date 객체의 생성자에 문자열 타입의 값을 사용하면, 내부적으로 Date.parse()를 호출하여 적절한 값을 계산해내며, 이 함수는 RFC2888 스펙과 ISO-8601 스펙을 지원한다. 하지만 MDN의 Date.parse() 문서에도 나와있듯이, 이 메소드의 결과값은 브라우저마다 구현상태가 다르고, 문자열의 형태에 따라 정확한 값을 예측하기 어렵기 때문에 사용하지 않기를 권장하고 있다.

예를 들어 2015-10-12 12:00:00 같은 문자열은 사파리나 IE의 경우 NaN 을 반환하고, 크롬이나 파이어폭스의 경우 로컬 타임존의 값을 반환하며, 경우에 따라 다른 환경에서는 UTC 기준의 값을 반환하기도 한다.

서버 데이터를 이용한 Date 객체 생성

이제, 서버로부터 데이터를 전달받는 경우를 생각해보자. 만약 데이터가 숫자형의 유닉스 시간값이라면, 간단하게 생성자를 이용해서 Date 객체를 생성할 수 있다. 앞에서는 설명을 생략했지만, Date 생성자는 인자로 숫자 하나만을 받으면 년도값으로 생각하지 않고, 밀리초 단위의 유닉스 시간으로 인식한다. (주의: 자바스크립트는 유닉스 시간값을 밀리초 단위로 다룬다. 즉, 초 단위로 계산된 값이라면 1000을 곱해주어야 한다) 아래의 예제를 보면 이전 예제와 동일한 결과값을 반환하는 것을 볼 수 있다.

var d1 = new Date(1489199400000);
d1.toString(); // Sat Mar 11 2017 11:30:00 GMT+0900 (KST)

그러면 유닉스 시간이 아닌 ISO-8601과 같은 문자열 타입은 어떨까? 앞 절에서 설명했듯이 Date.parse() 메소드는 결과를 신뢰할 수 없기 때문에 사용하지 않는 것을 권장하고 있다. 하지만 ECMAScript 5판부터 ISO-8601 을 지원하도록 명시되어 있기 때문에, ECMAScript 5판을 지원하는 IE9 이상의 브라우저에서는 주의해서 사용하면 ISO-8601 형식의 문자열을 Date 생성자에 사용할 수 있다.
여기서 주의할 점은, 최신 브라우저가 아닌 경우 마지막에 Z 문자가 없으면 UTC 기준으로 해석해야 함에도 불구하고 로컬 타임을 기준으로 해석하는 경우가 있다는 점이다. 아래의 예제는 IE10 에서 실행시킨 결과이다.

var d1 = new Date('2017-03-11T11:30:00');
var d2 = new Date('2017-03-11T11:30:00Z');
d1.toString(); // "Sat Mar 11 11:30:00 UTC+0900 2017"
d2.toString(); // "Sat Mar 11 20:30:00 UTC+0900 2017"

스펙에 따르면 두 결과값이 같아야 함에도 불구하고, d1.toString()d2.toString()의 결과값이 다른 것을 볼 수 있다. 최신 브라우저에서는 두 결과가 동일할 것이므로, 브라우저 버전별로 다르게 해석되는 문제를 막기 위해서는 타임존 데이터가 없는 경우 문자열의 마지막에 항상 Z를 추가해 주어야 한다.

서버로 전달할 데이터 생성

이제 앞서 생성된 Date 객체를 이용하면 로컬 타임존을 기준으로 날짜나 시간을 더하거나 빼는 등의 연산을 자유롭게 할 수 있다. 하지만 마지막에 다시 서버로 데이터를 전송하기 위해서는 데이터를 변환하는 과정이 필요하다.

먼저 유닉스 시간 형식의 경우 getTime() 메소드를 이용해서 간단하게 수행할 수 있다. (앞에서와 마찬가지로 밀리초 단위라는 것에 유의하자)

var d1 = new Date(2017, 2, 11, 11, 30);
d1.getTime(); // 1489199400000

ISO-8601 형식의 문자열은 어떨까? 앞서 말했듯이 ECMAScript 5판 이상을 지원하는 IE9 이상의 브라우저는 ISO-8601 형식을 지원하며, toISOString()toJSON() 메소드를 이용하면 ISO-8601 형식의 문자열을 생성할 수 있다. (toJSON()JSON.stringify()등에서 재귀적으로 호출될 때도 사용될 수 있다) 두 메소드의 결과값은 동일한데, 다만 다음과 같이 유효하지 않은 데이터에 대한 처리만 조금 다르다.

var d1 = new Date(2017, 2, 11, 11, 30);
d1.toISOString(); // "2017-03-11T02:30:00.000Z"
d1.toJSON();      // "2017-03-11T02:30:00.000Z"

var d2 = new Date('Hello');
d2.toISOString(); // Error: Invalid Date
d2.toJSON();      // null

이 외에도 UTC 기준의 문자열을 생성할 수 있는 toGMTString()toUTCString() 메소드가 있는데, 이들은 RFC-1123 표준에 맞는 문자열을 반환하므로 필요에 따라 사용할 수 있을 것이다.

Date 객체에는 toString(), toLocaleString() 및 이들의 확장 메소드들이 더 있지만, 주로 로컬 타임존 기준의 문자열을 반환하는 목적으로 사용되고, 특히 브라우저나 OS 환경에 따라 같은 결과가 보장되지 않기 때문에 사실상 별로 유용하지 않다.

로컬 타임존 변경하기

지금까지 살펴본 바에 의하면 자바스크립트에서도 어느정도 타임존 지원이 이뤄지고 있는 것 같다. 하지만 만약, OS의 타임존 설정을 따르지 않고, 어플리케이션 내에서 로컬 타임존을 직접 변경하고 싶다면 어떻게 해야 할까? 아니면, 다양한 타임존에서의 시간을 하나의 어플리케이션에서 동시에 보여주어야 한다면? 여러 번 말했듯이, 자바스크립트에서는 로컬의 타임존을 직접 변경할 수 없다. 유일하게 할 수 있는 방법은 원하는 타임존의 오프셋 값을 알고 있는 경우, 오프셋값을 더하거나 빼서 직접 날짜를 계산하는 것이다. 안된다고 무작정 포기하지 말고, 일단 자바스크립트가 할 수 있는 최선의 방법에 대해 살펴보자.

먼저, 앞선 예시에서 처럼 현재 브라우저의 타임존 설정이 서울이라고 가정하자. 사용자는 서울 기준의 2017년 3월 11일 오전 11시 30분 데이터를 뉴욕의 타임존 기준으로 볼 수 있기를 원한다. 그리고 서버에서는 해당 데이터를 밀리초 단위의 유닉스 시간으로 전송해 주면서, (친절하게도) 뉴욕의 오프셋이 -05:00 이라는 것도 알려주었다. 자, 그럼 현재 로컬 타임존의 오프셋만 알면 데이터를 변환할 수 있을 것이다.

이 때 사용할 수 있는 메소드가 바로 getTimeZoneOffset() 메소드이다. 이 메소드는 자바스크립트에서 로컬 타임존 정보를 알 수 있는 유일한 API라고 할 수 있는데, 현재 타임존의 오프셋을 분 단위의 숫자로 반환한다.

var seoul = new Date(1489199400000);
seoul.getTimeZoneOffset(); // -540

반환값 -540 은 타임존이 540분 앞서 있다는 의미이다. 서울의 오프셋이 +09:00 이란 걸 생각해보면 부호가 반대로 되어 있는 걸 알 수 있는데, 왜 그런지 모르겠지만 암튼 주의해야 한다. 이 방식을 기준으로 뉴욕의 오프셋 -05:00 을 계산해 보면 60 * 5 = 300 이 될 것이다. 이 840 만큼의 차이를 밀리초로 단위로 보정해서 새로운 Date 객체를 만들면, 그 객체의 getXX 메소드들을 이용해서 원하는 형태의 데이터를 만들어낼 수 있을 것이다. 간단한 포매터 함수를 만들어 결과를 비교해 보자.

function formatDate(date) {
  return date.getFullYear() + '년 ' + 
    (date.getMonth() + 1) + '월 ' + 
    date.getDate() + '일 ' + 
    date.getHours() + '시 ' + 
    date.getMinutes() + '분';
}

var seoul = new Date(1489199400000);
var ny = new Date(1489199400000 - (840 * 60 * 1000));

formatDate(seoul);  // 2017년 3월 11일 11시 30분
formatDate(ny);     // 2017년 3월 10일 21시 30분

formatDate() 의 결과에서 서울과 뉴욕의 시간대에 맞게 날짜가 잘 표시되고 있는 걸 볼 수 있다. 의외로 간단하게 해결된 것 같다. 자, 그러면 해당 지역의 오프셋만 알고 있다면 로컬 타임존을 변경할 수 있다는 것일까? 아쉽게도 답은 ‘아니오’다. 1부에서 타임존이 단순히 오프셋이 아니라 모든 오프셋 변경의 히스토리를 담고 있는 일종의 데이터베이스라고 했던 것이 기억나는가? 정확한 타임존 계산을 위해서는 단순히 현재 시점의 오프셋이 아닌, 해당 날짜가 가르키는 시점의 오프셋을 알아야 한다.

로컬 타임존 변경의 문제

위의 예제를 조금만 더 발전시키면 쉽게 문제를 발견할 수 있을 것이다. 사용자가 뉴욕의 시간대에서 해당 시간을 확인한 후에 날짜를 11일에서 15일로 변경하려고 한다. Date 객체의 setDate() 메소드를 이용하면 다른 항목들은 유지한 채로 날짜 값만 변경할 수 있다.

ny.setDate(15);
formatDate(ny);    // 2017년 3월 15일 21시 30분

간단히 해결되는 것 처럼 보이지만, 여기에는 함정이 있다. 먼저, 이 데이터를 다시 서버로 전송해야 한다면 어떻게 해야 할까? 우리는 데이터 자체를 변경했기 때문에, getTime() 이나 getISOString() 등의 메소드를 사용할 수 없다. 그러므로 서버로 전송하기 위해서는 앞에서 했던 계산을 역으로 해서 원래 데이터를 연산해야만 한다.

var time = ny.getTime() + (840 * 60 * 1000);  // 1489631400000

어차피 반환할 때 역으로 계산해야 하는데, 왜 굳이 변환된 데이터에서 날짜를 더하는지 궁금한 사람들이 있을 것이다. 그냥 기존 데이터에 연산을 하고, 포맷팅을 할 때만 임시로 변환된 Date 객체를 생성해도 될 것 같다. 하지만, 만약 서울기준의 Date 객체에서 날짜로 15로 변경했다면 11일에서 15일이 되었으므로 4일 (24 * 4 * 60 * 60 * 1000)이 추가된 것이고, 뉴욕 기준에서는 10일에서 15일이 되었으므로 5일 (24* 5 * 60 * 60 * 1000)이 추가된 것이다. 즉, 날짜 연산을 할 때에도 해당 오프셋을 기준으로 진행해야만 정확한 연산이 가능하다.

하지만 더 중요한 문제는, 단순히 오프셋을 더하고 빼는 것만으로는 해결되지 않는 문제가 있다는 것이다. 뉴욕의 타임존은 3월 12일부터 서머타임(DST)이 적용되기 때문에 2017년 3월 15일은 오프셋이 -05:00 이 아닌 -04:00 이 되어야 한다. 즉 역으로 연산할 때에는 현재보다 60분이 적은 780분 만큼만 더해주어야 하는 것이다.

var time = ny.getTime() + (780 * 60 * 1000);  // 1489627800000

반대로 사용자의 로컬 타임존이 뉴욕일 때 서울의 타임존 기준으로 날짜 연산을 하려고 하면, 불필요하게 서머타임이 적용되어 계산이 잘못되는 문제가 발생할 것이다.

결론적으로 단순히 전달받은 오프셋 값만 가지고는 원하는 타임존 기준으로 날짜 연산을 할 수가 없다. 뿐만 아니라, 1부의 내용을 다시 기억해보면 서머타임이 적용되는 규칙까지 안다고 해도 여전히 허점이 있다는 것을 알 것이다. 즉, 정확한 날짜 연산을 위해서는 IANA timezone Database와 같이 타임존 변경 히스토리가 담긴 전체 데이터가 필요한 것이다.

해결을 위해서는 전체 타임존 데이터베이스를 저장하고 있다가, Date 객체에서 날짜나 시간 데이터를 가져올 때마다 데이터베이스에서 해당 날짜와 타임존에 맞는 오프셋을 알아낸 다음, 위와 같은 연산을 통해 결과를 반환해야 할 것이다. 물론 이론적으로 가능은 하겠지만, 이를 위해서는 너무 많은 노력이 필요하고, 실제 변환된 데이터가 정확한지 테스트를 하기도 쉽지가 않을 것이다. 하지만, 너무 낙심은 말자. 이제 우리는 자바스크립트 타임존에 어떤 문제가 있는지, 그리고 어떻게 해결할 수 있는지를 알았다. 이제, 잘 만들어진 라이브러리를 찾아서 사용해도 되는 자격을 갖춘 것이다.

Moment Timezone

Moment는 자바스크립트에서 날짜 연산을 할 때 거의 표준처럼 자리잡은 라이브러리이다. 다양한 날짜 연산 및 포맷팅 API를 제공하며, 최근 많은 사용자들에 의해 널리 사용되면서 안정성도 검증받았다고 할 수 있다. 또한 Moment Timezone 이라는 확장 모듈을 이용하면 위에서 말한 문제들을 간단하게 해결할 수 있다. 이 확장 모듈은 실제 IANA timezone Database 의 데이터를 내장하여 실제 오프셋을 정확히 계산해주며, 타임존을 변경하고 포맷팅 할 수 있는 다양한 API를 제공한다.

이 글에서는 라이브러리의 사용법이나 구조에 대해 깊이 다루지는 않을 것이다. 다만, 위에서 우리가 발견했던 문제들을 얼마나 간단하게 해결할 수 있는지만 살펴보도록 하겠다. 관심 있는 분들은 해당 Moment Timezone의 문서를 참고하길 바란다.

자, 그럼 위의 문제를 Moment Timezone 을 이용해서 해결해 보자.

var seoul = moment(1489199400000).tz('Asia/Seoul');
var ny = moment(1489199400000).tz('America/New_York');

seoul.format(); // 2017-03-11T11:30:00+09:00
ny.format();    // 2017-03-10T21:30:00-05:00

seoul.date(15).format();  // 2017-03-15T11:30:00+09:00
ny.date(15).format();     // 2017-03-15T21:30:00-04:00

결과를 보면, seoul의 오프셋은 그대로인 반면에, ny의 오프셋은 -05:00 에서 -04:00 으로 변경된 것을 볼 수 있다. 또한, format() 함수를 이용하면 해당 오프셋이 정확히 반영된 ISO-8601 문자열을 얻을 수 있다. 앞의 절에서 들였던 노력에 비해 얼마나 간단한지 한눈에 체감이 될 것이다.

결론

지금까지 자바스크립트에서 지원하는 타임존 관련 API들과, 관련된 문제점들을 살펴보았다. 로컬 타임존을 직접 변경할 필요가 없다면, IE9 이상의 브라우저에 한해서 기본 API 만으로도 필요한 기능들을 구현할 수 있을 것이다. 하지만, 로컬 타임존을 직접 변경해야 한다면 문제는 복잡해진다. 서머 타임이 없고, 타임존 정책에 변경이 거의 없는 지역에 대해서는 위에서 설명한 것 처럼 getTimezoneOffset() 을 활용한 데이터 변환으로 불완전하게나마 구현이 가능하다. 하지만, 다양한 지역에 대한 타임존을 제대로 지원하고 싶다면, 직접 구현하려는 욕심을 버리고 Moment Timezone과 같은 라이브러리를 활용하는 것이 나을 것이다.

사실 1부의 서두에서도 밝혔듯이, 이 글은 타임존을 제대로 구현해 보겠다는 노력의 실패담으로 시작되었고, 예정된 결론은 “라이브러리를 사용하라” 였다. 하지만, 글을 쓰다보니 처음 예정했던 결론과는 조금 다르게 글을 마치게 되는 것 같다. 결론적으로, 실제 자바스크립트에서 타임존 관련 기능을 얼마나 지원하는지와 어떤 문제점이 있는지를 모른 채 무작정 외부 라이브러리에 의존하는 것은 결코 바람직한 접근이 아니라고 생각한다. 항상 그렇듯이, 각자의 상황에 맞게 적절한 도구를 선택하는 것이 중요하다. 이 글이 그런 판단을 내리는 데에 조금이나마 도움이 되었기를 바란다.

참고 링크

자바스크립트

자바스크립트에서 타임존 다루기 (1)

이 글은 NHNEnt 의 기술블로그인 토스트 밋업에 총 2부로 연재된 ‘자바스크립트에서 타임존 다루기’의 첫번째 글이다. 원문은 http://meetup.toast.com/posts/125 에서 확인할 수 있다.

  • 다음 글 : 자바스크립트에서 타임존 다루기 (2)

 

최근 나는 팀에서 관리하는 자바스크립트 캘린더 라이브러리에 타임존 기능을 추가하는 작업을 한 적이 있다. 자바스크립트의 타임존 지원이 다른 언어에 비해 부실하다는 얘기는 이미 들어 알고 있었지만, 기존 Date 객체를 잘 추상화하면 어렵지 않게 구현할 수 있을거라 생각했다.

하지만 작업을 진행할수록 자바스크립트에서 타임존을 다루는 것이 결코 쉬운 일이 아니란 것을 깨달았다. 특히 단순히 시간 정보를 포맷팅하는 정도가 아니라, 캘린더처럼 시간 정보에 대한 복잡한 연산이 더해지게 되면 타임존은 더욱 더 골칫덩어리가 된다. 덕분에 오랜만에 문제를 해결할 때마다 새로운 문제가 쏙 쏙 생겨나는 심장 쫄깃한 경험을 할 수 있었다.

이 글은 이러한 개인적인 경험을 바탕으로 자바스크립트에서 타임존을 다룰 때의 문제와 해결방법을 정리해 볼 예정이다. 글을 쓰면서 (왜 글이 점점 길어질까 고민하다가)알게 된 사실인데, 이 고난의 가장 큰 원인은 타임존이라는 도메인에 대한 이해가 부족했기 때문이라고 생각한다. 그래서 글을 1, 2부로 나누어 1부에서는 타임존과 관련 표준에 대해 자세히 알아보고 2부에서 본격적으로 자바스크립트와 관련된 내용을 풀어보도록 하겠다.

타임존이란?

타임존은 동일한 로컬 시간을 따르는 지역을 의미하며, 주로 해당 국가에 의해 법적으로 지정된다. 보통 국가별로 각자의 고유한 타임존을 사용하고 있으며, 미국이나 캐나다처럼 면적이 넓은 나라인 경우 지역별로 각기 다른 타임존을 사용하기도 한다. (반면 중국은 그 넓은 면적에도 불구하고 하나의 타임존을 이용하고 있는데, 이로 인해 중국 서쪽 지역에서는 오전 10시는 되어야 해를 볼 수 있다)

GMT, UTC 그리고 오프셋(Offset)

GMT

한국의 타임존은 보통 GMT+09:00 으로 표현된다. 여기서 GMT는 Greenwich Mean Time의 약자로서 경도 0도에 위치한 영국 그리니치 천문대를 기준으로 하는 태양 시간을 의미한다. GMT 시간은 1925년 2월 5일부터 사용하기 시작하였으며, 1972년 1월 1일까지 세계 표준시로 사용되었다.

UTC

GMT는 또한 UTC라고도 불리는데, 두 용어가 혼용되어서 사용되고 있기는 하지만 엄밀히 구분하자면 둘은 다른 의미를 가진다. UTC는 지구 자전주기의 흐름이 늦어지고 있는 문제를 해결하기 위해 1972년에 세슘 원자의 진동수에 기반한 국제 원자시를 기준으로 다시 지정된 시간대이다. 즉, UTC는 좀더 정확한 시간측정을 위해서 GMT를 대체하기 위해 제정된 새로운 표준이며, 시간적으로는 둘 사이에 아주 미세한 차이밖에 없지만, 소프트웨어에서 사용할 때는 UTC라고 하는 것이 더 정확한 표현일 것이다.

UTC라는 약자가 탄생한 배경은 좀 재미있는데, 처음에 영어권에서는 CUT(Coordinated Universal Time)를, 프랑스어권에서는 TUC(Temps Universel Coordonn)를 사용하기를 원했다고 한다. 하지만 결국 한쪽이 이기지는 못하고, 두 언어 모두 C, T, U로 되어 있으니 중재안으로 UTC가 채택되었다고 한다.

오프셋

UTC+09:00 에서 +09:00 의 의미는 UTC의 기준시간보다 9시간이 빠르다는 의미이다. 즉 UTC 기준으로 현재 낮 12시라면 한국시간으로는 오후 9시가 될 것이다. 이렇게 UTC와의 차이를 나타낸 것을 오프셋이라고 하며, +09:00 혹은 -03:00 등과 같이 표현된다.

보통 국가나 지역들마다 자신들이 사용하는 타임존에 대해 고유의 이름을 부여한다. 예를 들어 대한민국의 타임존은 KST(Korea Standard Time)이라고도 불리는데, 이는 앞서 설명했듯이 특정 오프셋을 지칭하므로 KST = UTC+09:00 이라고 이해하면 된다. 하지만 +09:00 오프셋은 한국 뿐만 아니라 일본, 인도네시아 등 여러 지역에서 사용하고 있으므로, 오프셋과 타임존 이름들의 관계는 1:1 관계가 아닌 1:N 관계이다. +09:00를 사용하는 국가 혹은 지역의 목록은 UTC+09:00에서 확인할 수 있다.

오프셋은 한시간 단위가 아닌 경우도 있다. 예를 들어 북한은 +08:30을 기준시로 사용하고 있고, 호주의 경우 지역에 따라 +08:45, 혹은 +09:30을 기준시로 이용하고 있다.

전체 UTC 오프셋 및 이들 이름의 목록은 List of UTC Time offsets에서 확인할 수 있다.

타임존 !== 오프셋?

앞서 말했듯이 우리가 타임존을 말할 때는 보통 KST, JST(Japan Standard Time) 등과 같이 오프셋과 동일한 의미로 쓰인다. 하지만, 특정 지역의 타임존을 단순히 오프셋이라고 지칭하기는 어려운데, 이는 아래의 두 가지 이유 때문이다.

서머 타임 (DST)

국내에서는 여전히 생소한 개념이지만, 해외 여러 국가에서는 서머 타임(summer time)이 존재한다. 사실 서머 타임은 주로 영국에나 유럽에서 쓰이는 용어인데, 좀더 범용적인 용어로는 일광 시간 절약제, 즉 DST(Daylight Saving Time)라고 불린다. 이는 하절기에 표준시를 원래 시간보다 한 시간 앞당긴 시간으로 이용하는 것을 의미한다.

위키피디아에 따르면 국내에서도 1948 ~ 1951, 1955 ~ 1960, 1987 ~ 1988년에 DST가 시행된 적이 있다고 한다.

예를 들어 미국의 캘리포니아 지역은 평소에는 PST(Pacific Standard Time)을 기준시로 이용하고, 하절기에는 PDT(Pacific Daylight Time, UTC-07:00)를 기준시로 이용한다. 두 시간대를 사용하는 타임존을 묶어서 Pacific Time(PT)라고 부르기도 하며, 미국의 여러 지역 및 캐나다 일부 지역에서도 사용하고 있다.

그러면 이 DST가 적용되는 하절기는 정확히 언제를 의미하는걸까? 사실 DST 적용에는 보편적인 규칙이 있는 것이 아니고, 국가나 지역의 법에 따라서 다르게 적용된다. 예를 들어 미국과 캐나다는 2006년까지 각 지역시간 기준으로 4월의 첫 일요일 오전 2시에 시작하여 10월의 마지막 일요일 오전 12시에 해제하였지만, 2007년부터는 3월의 두 번째 일요일 오전 2시에 시작하여 11월 첫 일요일 2시에 해제하도록 변경되었다. 그리고 유럽에서는 DST가 일괄적으로 적용되는 반면, 미국에서는 각 지역의 시간대별로 순차적으로 적용된다.

타임존은 변한다?

앞에서도 잠깐 언급했었지만, 각 지역이 어떤 타임존을 이용할 지는 해당 지역 혹은 국가가 법적으로 결정하기 때문에, 정치적 혹은 경제적 이유로 변경될 수 있다. 예를 들어, 미국에서 2007년부터 DST의 적용시점이 변경된 것은 미국 2005년에 조지 부시 대통령이 서명한 에너지 정책법에 의해서였다. 또한 이집트나 러시아는 기존에 DST를 이용했었지만, 2011년부터 DST를 이용하지 않기로 결정했다.

DST 뿐만 아니라 국가의 표준시가 변경되기도 하는데, 예를 들어 사모아 섬의 경우 원래 UTC-10:00 의 오프셋을 기준시로 사용했었지만, 2011년에 오스트레일리아, 뉴질랜드와의 무역에서 날짜 차이에 따른 손실을 줄이기 위해 UTC+14:00 로 변경하면서 하루를 앞당겼고, 이로 인해 2011년 12월 30일이 사라지게 되면서 크게 뉴스가 되기도 했었다. (뉴스 링크)

네덜란드의 경우에는 1909년부터 +0:19:32.13 이라는 필요 이상으로 정확한 오프셋을 사용하고 있었는데, 1937년부터 +00:20 으로 변경했다가, 1940년부터는 +01:00 을 기준시로 사용하고 있다.

타임존 1 : 오프셋 N

위에서 살펴본 내용을 정리해보자면, 한 지역의 타임존은 하나 혹은 그 이상의 오프셋을 가지며, 어느 시점에 어떤 오프셋을 기준시로 이용할지는 해당 지역의 정치/경제적 상황에 따라 계속해서 달라진다고 할 수 있다.

일상 생활에서는 이런 상황이 큰 문제가 없을지도 모르지만, 이를 규칙에 따라 시스템화 시키려고 하면 문제가 발생한다. 내 스마트폰의 기준시를 오프셋을 이용해서 지정한다고 생각해보자. 만약 내가 DST가 적용되는 지역에 살고 있다면 스마트폰의 기준시를 DST가 적용되고 해제될 때마다 변경해 주어야 할 것이다. 이런 경우 위에서 언급했던 Pacific Time 처럼 표준시와 DST를 묶어서 하나의 타임존으로 인식할 수 있는 또 다른 개념이 필요할 것이다.

하지만, 이를 단순히 몇가지 규칙으로 지정하기는 어렵다. 예를 들어 미국의 경우 2007년을 기준으로 DST 를 적용하는 시점이 변경되었기 때문에, 2006년 3월 31일은 PDT(-07:00)가 기준시가 되지만, 2007년 3월 31일은 PST(-08:00)가 기준시가 되어야 할 것이다. 즉, 특정 지역의 타임존을 지칭하기 위해서는 역사적으로 표준시간대 혹은 DST 적용 룰이 언제 변경되었는지에 대한 데이터를 모두 갖고 있어야만 정확한 시간을 계산할 수 있는 것이다.
즉, 우리는 “뉴욕의 타임존은 PST(-08:00)야” 라고 말할 수 없다. 약간 더 정확히 말하자면 “뉴욕의 타임존은 현재 PST야” 라고 할 수 있을 것이다. 하지만 시스템의 관점에서 더 엄밀하게 말하자면 타임존이라는 단어 없이 “뉴욕은 현재 PST를 기준시로 사용하고 있어” 라고 말하는 것이 가장 정확할 것이다.

그러면 우리는 특정 지역의 타임존을 오프셋이 아닌 무엇으로 지칭해야 할까? 바로 지역명이다. 정확히 이야기하면 역사적으로 표준시간대나 DST의 변경이 동일하게 적용되었던 지역을 하나의 타임존으로 묶어서 지칭할 수 있을 것이다. 앞서 잠깐 언급했던 PT(Pacific Time)과 같은 명칭이 이용될 수도 있겠지만, 이는 현재의 표준시와 DST만을 묶어놓은 개념이기 때문에 역사적인 변경내역을 모두 포함한다고 할 수는 없을 것이다. 또한 PT는 미국/캐나다 지역에서만 사용되는 이름이기 때문에, 소프트웨어에서 범용적으로 사용하기 위해서는 신뢰할 수 있는 기관에서 관리되는 표준이 필요하게 된다.

IANA time zone database

사실 타임존에 대한 표준은 단순히 규칙이기 보다는 데이터베이스에 가까운데, 역사적인 변경 내역을 모두 저장하고 있어야 하기 때문이다. 이러한 표준 데이터베이스는 여럿이 있지만, 현재로서 가장 신뢰할 수 있는 표준은 바로 IANA time zone database 이다. IANA time zone database는 보통 tz database (혹은 tzdata) 라고 불리며, 전 세계 모든 지역의 표준시와 DST 변경 내역을 담고 있다. 현재 역사적으로 확인할 수 있는 모든 데이터가 들어있다고 볼 수 있는데, UNIX 시간(1970.01.01 00:00:00) 이후의 데이터의 정확도를 보장하도록 정리되었다. (즉, 1970년 이전의 데이터도 있지만 이 시기의 데이터는 완벽한 정확성을 보장하지는 않는다)

이름은 Area / Location 의 규칙을 이용하는데, Area 는 보통 대륙이나 대양명 (Asia, America, Pacific 등)을 지정하며, Location 은 주로 국가명보다는 큰 도시 위주 (Seoul, New_York 등)로 지정된다. (도시명이 국가명보다는 더 영구적인데, 국가의 수명은 생각보다 짧다)
예를 들어 대한민국의 타임존은 Asia/Seoul 이고, 일본의 경우 Asia/Tokyo 인데, 현재 두 지역 모두 UTC+09:00을 표준시로 사용하고 있지만, 실제 역사적인 변경 내역이 다르고 현재 다른 국가에 속해있기에, 별도의 타임존으로 관리되는 것이다.

IANA time zone database는 많은 개발자들과 역사학자들의 커뮤니티들에 의해 관리되고 있으며, 역사적 발견이 추가되거나 정부 정책이 바뀌는 경우 바로 갱신되기 때문에 신뢰도가 가장 높다. 또한 리눅스, macOS등 유닉스 기반의 OS들이나 자바, PHP 등 유명 프로그래밍 언어들 등 이미 많은 시스템에서 이 데이터베이스를 내부적으로 사용하고 있다.

위의 지원 환경 목록에 Windows가 없는 것을 눈치챈 사람이 있을 것이다. 사실, Windows는 별도의 데이터베이스를 내장하고 있는데, 이를 Microsoft Time Zone Database라 부른다. 하지만 여기에는 역사적인 변경 내역들이 누락된 부분이 많고, Microsoft라는 회사에 의해서만 관리되기 때문에, 정확도나 신뢰도 면에서 IANA에 비해 떨어진다고 볼 수 있다.

자바스크립트와 IANA time zone database

서두에서 잠깐 언급했지만, 자바스크립트는 사실 타임존 지원이 미약한 편이다. 기본적으로는 현재 지역의 (좀더 정확히 이야기하자면 설치된 OS에 설정된 타임존)을 따르게 되어 있기 때문에, 명시적으로 타임존을 변경할 수 있는 방법이 없다. 뿐만 아니라 데이터베이스 표준에 대한 명세도 명확하지 않은데, 실제 ES2015의 스펙을 살펴보면 로컬 타임존과 DST의 적용에 대해 두세 줄 정도의 모호한 정의만 되어 있을 뿐이다. 예를 들면 DST의 경우 다음과 같이 정의되어 있다. 링크 : ECMAScript 2015 – Daylight Saving Time Adjustment

An implementation dependent algorithm using best available information on time zones to determine the local daylight saving time adjustment DaylightSavingTA(t), measured in milliseconds. An implementation of ECMAScript is expected to make its best effort to determine the local daylight saving time adjustment.

뭔가… 그냥 최선을 다해서 정확히 구현하라는 의미로만 보인다 (-_-). 즉, 구현하는 브라우저 벤더마다 구현이 달라질 수도 있다는 의미이다. 이런 무책임한! 이라는 생각이 들 때쯤 바로 밑에 NOTE 한 줄이 추가되어 있는 것이 눈에 보인다.

NOTE : It is recommended that implementations use the time zone information of the IANA Time Zone Database http://www.iana.org/time-zones/.

그렇다. ECMA 스펙은 이 한 줄로 책임을 살짝 피해가려 했겠지만, 결론적으로 자바스크립트에서는 사실상 명시된 표준 데이터베이스가 없고, 단지 IANA Time Zone Database가 권장될 뿐이다. 당연한 이야기지만 이로 인해 실제로 브라우저마다 타임존 연산이 다르게 동작하는 경우도 있다. 나중에 국제화 API를 위한 ECMA-402 스펙의 Intl.DateTimeFormat에서 IANA timezone을 사용하는 옵션이 추가되긴 했지만, 여전히 다른 언어에 비해서는 신뢰할만한 타임존 지원이 많이 부족하다고 볼 수 있다.

1부를 정리하며

지금까지 살펴본 것처럼, 타임존은 생각보다 복잡한 개념이며 이를 정확하게 계산하기 위해서는 단순한 연산 뿐만 아니라 표준화된 데이터베이스가 필요하다. 이러한 작업을 언어 차원의 지원 없이 직접 구현하기는 쉽지 않은데, 안타깝게도 자바스크립트는 이러한 지원이 상당히 적은 편이다. 2부에서는 실제 자바스크립트에서 타임존을 다룰 때 발생하는 문제와 이를 어떻게 해결할 수 있을지를 알아보도록 하겠다.

참고 링크