자바스크립트

자바스크립트에서 타임존 다루기 (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부에서는 실제 자바스크립트에서 타임존을 다룰 때 발생하는 문제와 이를 어떻게 해결할 수 있을지를 알아보도록 하겠다.

참고 링크

자바스크립트

[번역] 웹어셈블리의 현재 위치와 미래

이 글은 모질라 Hacks의 웹어셈블리 시리즈 중 마지막 글인 Where is WebAssembly now and what’s next?의 번역글이다. 전 시리즈에 걸쳐 웹어셈블리 뿐만 아니라 JIT나 어셈블리 등에 대해서도 체계적으로 잘 설명하고 있고, 카툰까지 곁들여서 이해하기도 쉽다.

총 6부로 되어 있으며, 원작자의 허가를 얻어 전체 시리즈를 모두 번역했으니 처음부터 차례대로 모두 읽어보길 권한다.

  1. 카툰으로 소개하는 웹어셈블리
  2. 저스트-인-타임(JIT) 컴파일러 집중 코스
  3. 어셈블리 집중 코스
  4. 웹어셈블리 모듈의 생성과 동작
  5. 웹어셈블리는 왜 빠를까?
  6. 웹어셈블리의 현재 위치와 미래 (현재글)

 

2월 28일, 4개의 주요 브라우저들은 웹어셈블리의 MVP가 완료되었다는 합의에 이르렀다고 발표했다. 이는 브라우저들이 탑재를 시작할 수 있는 최초의 안정적인 버전을 제공한다.

logo_party01-500x169.png

이는 브라우저가 탑재할 수 있는 안정적인 코어를 제공한다. 이 코어는 커뮤니티 그룹이 계획하고 있는 모든 기능을 포함하고 있지는 않지만, 웹어셈블리를 빠르고 사용가능하도록 만들기에는 충분하다.

이것을 이용하면, 개발자들은 웹어셈블리 코드를 탑재할 수 있다. 이전 버전의 브라우저를 위해서 개발자들은 asm.js 버전의 코드를 내려보낼 수 있다. asm.js는 자바스크립트의 서브셋이기 때문에, 모든 JS 엔진이 이를 실행할 수 있다. Emscripten을 이용하면 동일한 앱을 웹어셈블리와 asm.js로 컴파일 할 수 있다.

최초 버전임에도 불구하고, 웹어셈블리는 빠를 것이다. 하지만 수정과 새로운 기능들이 더해지면, 앞으로는 더 빨라질 것이다.

브라우저에서 웹어셈블리 성능 향상시키기

브라우저들이 엔진상의 웹어셈블리 지원을 개선할수록 속도는 더 빨리질 것이다. 브라우저 벤더들이 이러한 이슈들을 위해 독자적으로 노력하고 있다.

JS와 웹어셈블리 간의 더 빠른 함수 호출

현재 JS 코드에서 웹어셈블리 함수를 호출하는 것은 필요 이상으로 느리다. 이는 트램폴린(trampolining)”이라는 것이 필요하기 때문이다. JIT는 웹어셈블리를 직접 어떻게 다루어야 할 지 모르기 때문에, 그 역할을 하는 것에게 웹어셈블리를 전달해야 한다. 이것은 엔진 내부에 있는 코드중 느린 부분인데, 최적화된 웹어셈블리 코드를 실행하기 위한 셋업 작업을 한다.

06-02-trampoline01-500x399.png

이 작업은 JIT가 웹어셈블리를 직접 어떻게 다룰지 아는 경우에 비해 100배 이상 느릴 수 있다.

만약 당신이 웹어셈블리 모듈에게 하나의 큰 작업만 넘겨준다면 이러한 오버헤드를 눈치채지 못할 수도 있다. 하지만 만약 웹어셈블리와 JS간에 빈번한 교류가 발생한다면(작은 여러개의 작업을 이용해서) 이 오버헤드가 두드러질 것이다.

더 빠른 로딩 시간

JIT는 더 빠른 로딩 시간과 더 빠른 실행 시간 사이의 트레이드-오프를 관리해야 한다. 만약 당신이 미리 컴파일하고 최적화하는 데에 더 많은 시간을 사용한다면, 실행 시간은 빨라질 수 있겠지만, 초기 구동 시간은 느려지게 된다.

코드가 한 번 실행된 후에 더 이상 끊김이 없도록 보장하기 위해 미리 컴파일을 하는 것과 대부분의 코드는 충분히 자주 실행되지 않기 때문에 최적화하는 것이 큰 의미가 없다는 사실 사이의 균형을 유지하기 위해 많은 작업들이 진행되고 있다.

웹어셈블리는 어떤 타입들이 사용될 지 추측할 필요가 없기 때문에, 실행 시점에 어떤 타입들이 사용되는지를 엔진이 모니터링할 필요가 없다. 이는 엔진에게 더 많은 선택지를 주는데, 예를 들면 컴파일을 실행과 병행해서 진행할 수도 있다.

게다가, 최근에 추가된 자바스크립트 API는 스트림을 이용해서 웹어셈블리를 컴파일할 수 있도록 해 준다. 즉, 바이트를 다운로드 받는 동안에 엔진이 컴파일을 시작할 수 있게 된다.

파이어폭스에서 우리는 두가지의 컴파일러 시스템을 작업하고 있다. 하나는 미리 실행되어 꽤 괜찮은 수준까지 코드를 최적화한다. 코드가 실행되는 동안에는 다른 컴파일러가 백그라운드에서 완벽하게 최적화를 한다. 완전히 최적화된 코드가 준비되면 기존 코드와 교체되어 들어간다.

MVP 이후 스펙에 추가될 기능들

웹어셈블리의 목표 중 하나는 모든 것을 한 번에 디자인하는 것보다는, 작은 조각으로 나누어 구체화하면서 계속해서 테스트하는 것이다.

즉, 기대되는 기능들이 있긴 하지만, 이들이 미리 100% 완벽하게 구체화되지는 않았다는 의미이다. 이들은 모든 브라우저 벤더들이 참여해서 스펙화하는 과정을 거쳐야만 할 것이다.

이 기능들은 미래 기능(future features)이라 불린다. 몇 가지만 살펴보겠다. 

DOM 직접 다루기

현재로선 DOM과 상호작용할 수 있는 방법이 존재하지 않는다. 즉, 웹어셈블리에서는  element.innerHTML과 같은 식으로 노드를 갱신할 수 없다는 의미이다. 

대신에, JS를 통해서 값을 설정하도록 해야 한다. 이 말은 자바스크립트 호출자(caller)에게 값을 다시 전달한다는 의미가 될 수도 있다. 아니면, 웹어셈블리 내부에서 자바스크립트 함수를 호출한다는 의미가 될 수도 있다. (자바스크립트와 웹어셈블리 함수 모두 웹어셈블리 모듈에서 임포트해서 사용할 수 있다)

06-03-dom01-500x418.png

어느 방법이든, 자바스크립트를 통하는 것은 직접 접근하는 것보다는 느릴 것이다. 일부 웹어셈블리 어플리케이션들은 이 문제가 해결될 때까지 기다려야 할 수도 있을 것이다.

공유 메모리의 동시성

코드의 속도를 향상시키는 한가지 방법은 코드의 다른 부분들이 동시에 병렬적으로 실행될 수 있게 만드는 것이다. 하지만 이 방법은 가끔 역효과를 일으키는데, 쓰레드간의 커뮤니케이션으로 인한 오버헤드가 원래 하려는 작업보다 더 많은 시간이 걸릴 수 있기 때문이다.

하지만 쓰레드간에 메모리를 공유할 수 있다면 이러한 오버헤드를 줄일 수 있다. 이를 위해 웹어셈블리는 자바스크립트에서 새롭게 추가된 SharedArrayBuffer를 사용할 것이다. 이 기능이 브라우저에서 제공되기 시작하면 웹어셈블리가 어떻게 이를 다룰지에 대해 작업 그룹이 스펙을 구체화할 수 있을 것이다.

SIMD

만약 웹어셈블리에 대한 다른 글을 읽거나 발표를 본 적이 있다면, 아마도 SIMD 지원에 대해서 들어보았을 것이다. SIMD는 단일 명령, 다중 데이터 (single instruction, multiple data)의 약자이다. 이는 병렬 실행을 위한 또다른 방법이다.

SIMD는 여러 수들로 이루어진 벡터와 같은 대량의 데이터 구조를 취해서, 각기 다른 부분에 동일한 명령을 동시에 적용할 수 있도록 해 준다. 이 방식으로 게임이나 VR에 필요한 복잡한 연산에 대한 성능을 급격하게 향상시킬 수 있다.

보통의 웹 앱 개발자들에게는 이것이 크게 중요하지 않을 것이다. 하지만 게임과 같은 멀티미디어 관련 개발자들에게 이 기능은 아주 중요하다.

예외 처리

C++과 같은 언어들의 많은 코드 베이스는 예외(exception)를 사용한다. 하지만 예외는 웹어셈블리의 일부로써 정의되어 있지 않다.

만약 Emscripten을 이용해 코드를 컴파일한다면, 컴파일러 최적화 단계에서 예외 처리를 에뮬레이트 해 줄 것이다. 하지만 꽤나 느리기 때문에, DISABLE_EXCEPTION_CATCHING 플래그를 이용해서 이 기능을 끄고 싶을 수도 있을 것이다. 

웹어셈블리에서 직접 예외 처리를 지원하게 된다면 이러한 에뮬레이션은 더이상 필요없을 것이다.

그 외에 개발자 편의를 위한 개선들

몇가지 미래 기능들은 성능에 영향을 끼치지는 않지만, 개발자들이 웹어셈블리를 다루기 쉽게 만들어 줄 것이다.

  • 일급 소스 레벨 개발자 도구. 현재 브라우저에서 웹어셈블리를 디버깅 하는 것은 순수 어셈블리를 디버깅하는 것이나 마찬가지일 것이다. 하지만 아주 소수의 개발자들만이 머릿속에서 자신의 코드를 어셈블리로 맵핑할 수 있다. 우리는 개발자들이 소스코드를 디버깅하는 것을 지원하는 도구를 어떻게 개선할 수 있을지 조사하고 있다.
  • 가비지 컬렉션. 만약 타입을 미리 정의할 수 있다면, 그 코드를 웹어셈블리로 변환할 수도 있어야만 한다. 그러므로 타입스크립트 등을 이용한 코드는 웹어셈블리와 호환 가능해야만 한다. 하지만 현재 딱 한가지 걸림돌이 있는데, 그것은 바로 웹어셈블리가 JS 엔진에 내장된 것과 같은 기존의 가비지 컬렉터와 어떻게 상호작용해야 하는지를 모른다는 점이다. 이 미래 기능의 아이디어는, 웹어셈블리에게 내장된 GC에 대한 일급 접근 권한과 하위 레벨 GC 원시 타입 및 연산을 제공하는 것이다.
  • ES6 모듈 통합. 브라우저는 현재 script 태그를 이용해서 자바스크립트 모듈을 로딩할 수 있도록 지원을 추가하고 있다. 이 기능이 추가된 후에는 <script src=url type=module> 같은 태그에서 url이 웹어셈블리 모듈을 가리킨다고 해도 동작할 수 있을 것이다.

결론

웹어셈블리는 지금도 빠르지만, 브라우저가 새로운 기능과 개선 사항을 구현하고 나면 더욱 더 빨라질 것이다.

Lin Clark 에 대해

Lin은 모질라 개발자 관계 팀의 엔지니어이다. 그녀는 자바스크립트, 웹어셈블리, Rust, Servo 등을 끄적거리며, 코드 카툰을 그린다.

코드카툰 : http://code-cartoons.com/
트위터 : @linclark

자바스크립트

[번역] 웹어셈블리는 왜 빠를까?

이 글은 모질라 Hacks의 웹어셈블리 시리즈 중 다섯 번째 글인 What makes WebAssembly fast?의 번역글이다. 전 시리즈에 걸쳐 웹어셈블리 뿐만 아니라 JIT나 어셈블리 등에 대해서도 체계적으로 잘 설명하고 있고, 카툰까지 곁들여서 이해하기도 쉽다.

총 6부로 되어 있으며, 원작자의 허가를 얻어 전체 시리즈를 모두 번역했으니 처음부터 차례대로 모두 읽어보길 권한다.

  1. 카툰으로 소개하는 웹어셈블리
  2. 저스트-인-타임(JIT) 컴파일러 집중 코스
  3. 어셈블리 집중 코스
  4. 웹어셈블리 모듈의 생성과 동작
  5. 웹어셈블리는 왜 빠를까? (현재글)
  6. 웹어셈블리의 현재 위치와 미래

 

이전 글에서 나는 웹어셈블리와 자바스크립트는 양자택일의 문제가 아니라는 점을 설명했다. 우리는 너무 많은 개발자들이 웹어셈블리만을 이용해 코드를 작성하기를 바라지 않는다.

그러므로 개발자들이 어플리케이션을 개발할 때 웹어셈블리와 자바스크립트 사이에서 하나를 선택할 필요는 없다. 다만, 자바스크립트 코드의 일부를 웹어셈블리로 교체할 수는 있을 것이다.

예를 들어, 리액트를 개발하고 있는 팀은 그들의 리콘사일러(reconciler) 코드 (일명 Virtual DOM) 를 웹어셈블리 버전으로 교체할 수 있을 것이다. 리액트를 사용하는 사람들은 아무것도 할 필요가 없다. 그들의 앱은 웹어셈블리의 장점을 취한다는 것 빼고는 이전과 정확하게 똑같을 것이다.

리액트 팀과 같은 개발자들이 이런 식의 교체를 할 만한 이유는 바로 웹어셈블리가 빠르기 때문이다. 그런데, 웹어셈블리는 왜 빠른걸까?

오늘날의 자바스크립트 성능은 어떨까?

자바스크립트와 웹어셈블리의 성능차이를 이해하려면, 먼저 자바스크립트 엔진이 하는 일을 이해해야 한다.

이 다이어그램은 오늘날 어플리케이션의 시작(start-up) 성능을 간략하게 보여준다.

자바스크립트 엔진이 각각의 작업들을 진행할 때 걸리는 시간은 해당 페이지가 사용하는 자바스크립트에 따라 다를 것이다. 이 다이어그램은 정확한 성능 수치를 나타내지 않는다. 대신, 같은 기능을 하는 자바스크립트와 웹어셈블리가 어떤 성능차이를 갖는지에 대한 고차원의 모델을 의미한다.

05-01-diagram_now01-500x129.png

각각의 막대는 특정 작업을 할 때 걸리는 시간을 나타낸다.

  • 구문해석 (Parsing) – 소스코드를 인터프리터가 실행할 수 있는 형태로 가공하는데 걸리는 시간
  • 컴파일 + 최적화 – 기본(baseline) 컴파일러와 최적화(optimizing) 컴파일러에서 걸리는 시간. 일부 최적화 컴파일러의 작업은 메인 스레드에서 진행되지 않기 때문에 여기에서 제외된다.
  • 재-최적화 (Re-optimizing) – JIT(Just in time Compiler)가 그들의 가정이 틀렸을 때 재조정하는데 걸리는 시간. 이는 다시 최적화를 하거나, 기본코드로 되돌리는 작업을 모두 포함한다.
  • 실행 – 코드를 실행하는 데 걸리는 시간
  • 가비지 컬렉션 – 불필요한 메모리를 비우는 데 걸리는 시간

한가지 중요한 사실 : 이 작업들은 구분된 단위를 갖거나 특정한 순서로 진행되지 않고, 서로 맞물려서 진행된다. 구문 해석이 일부 진행된 후, 일부 실행, 일부 컴파일, 다시 일부 구문 해석, 일부 실행 등의 형태로 진행된다.

이렇게 작은 단위로 분해함으로써, 다음 다이어그램과 같았던 초기 자바스크립트의 성능으로부터 큰 개선을 이룰 수 있었다.

05-02-diagram_past01-500x147.png

단순히 자바스크립트를 실행하는 인터프리터였던 초기에는 실행속도가 상당히 느렸다. JIT가 도입된 이후로 실행속도는 급격하게 증가했다.

트레이드-오프는, 코드를 감지하고 컴파일할 때 부하가 발생한다는 점이다. 만약 자바스크립트 개발자들이 지금까지 해왔던 방식으로만 코드를 작성한다면 구문해석과 컴파일 시간은 아주 작을 것이다. 하지만 자바스크립트의 성능이 개선될수록 개발자들은 자바스크립트로 더 복잡한 어플리케이션을 개발하게 될 것이다. 즉, 좀 더 개선할 여지가 있다는 의미이다.

웹어셈블리는 어떻게 다를까?

아래 다이어그램은 웹어셈블리가 전형적인 웹 어플리케이션과 어떻게 다른지를 보여준다.

05-03-diagram_future01-500x214.png

각각의 단계를 어떻게 다루는지는 브라우저마다 조금씩 다르다. 여기서는 SpiderMonky를 모델로 설명하겠다.

가져오기 (Fetching)

다이어그램에는 나오지 않았지만, 단순히 서버에서 파일을 가져오는 일에도 시간이 소모된다.

웹어셈블리는 자바스크립트보다 더 간결하기 때문에 데이터를 가져오는 속도가 더 빠르다. 압축 알고리즘이 자바스크립트 번들 파일의 사이즈를 상당히 줄일 수 있을지는 몰라도, 압축된 바이너리 형태의 웹어셈블리가 여전히 더 작을 것이다.

즉, 서버에서 클라이언트로 전송하는 시간이 더 적게 걸리게 된다. 느린 네트워크 환경에서는 더 의미가 있을 것이다.

구문해석 (Parsing)

자바스크립트 소스가 브라우저에 전달되게 되면, 추상 구문 트리(AST : Abstract Syntax Tree)로 변환된다.

브라우저는 종종 이 작업을 지연해서 실행하는데, 초기 실행에 필요한 부분만 먼저 해석하고 아직 호출되지 않은 함수들은 스텁(stub)만 만들어 둔다. 이때 AST는 중간 표현 형식(바이트코드라고 불리는)으로 변환되는데, 이는 각 자바스크립트 엔진에 따라 다르다. 반면, 웹어셈블리는 그 자체로 이미 중간 표현 형식이기 때문에 이 변환 작업을 거칠 필요가 없다. 웹어셈블리는 단지 해독된(decoded) 이후에 에러가 없는지만 검증되면(validated) 된다.

05-04-diagram_compare02-500x169.png

컴파일 + 최적화

JIT에 대한 글에서 설명했듯이, 자바스크립트는 코드를 실행하는 도중에 컴파일된다. 런타임에 어떤 타입이 사용되는지에 따라 동일한 코드가 다양한 버전으로 컴파일되기도 한다.

브라우저들은 웹어셈블리를 각자 다른 방식으로 컴파일한다. 일부 브라우저들은 실행하기 전에 기본 컴파일 단계를 거치고, 다른 브라우저들은 JIT를 사용한다.

어떤 방식이든, 웹어셈블리는 기계 코드와 훨씬 가까운 상태에서 시작한다. 예를 들면 프로그램의 일부로써 타입이 포함되어 있는데, 이는 다음의 몇가지 이유로 더 빠르다.

  1. 최적화된 코드를 컴파일하기 전에, 어떤 타입이 사용되었는지 확인하기 위해 코드를 실행해볼 필요가 없다.
  2. 같은 코드에서 대해서 여러가지 타입들이 사용되고 있을 때, 여러가지 버전으로 컴파일할 필요가 없다.
  3. LLVM 에서 이미 많은 최적화가 진행된 상태이기 때문에, 컴파일이나 최적화에 필요한 작업이 적다.

05-05-diagram_compare03-500x175.png

재-최적화 (Reoptimizing)

JIT는 가끔 최적화된 버전을 버리고, 다시 최적화해야한다.

이는 JIT가 코드 실행에 기초하여 만들어낸 가정이 잘못된 것으로 판명될 때 발생한다. 예를 들어 순환문 내부에서 사용되는 변수에 이전 주기와는 다른 값이 할당되거나, 프로토타입 체인내부에 다른 함수가 추가되면, 역최적화(deoptimization)가 발생한다.

역최적화에는 두가지 비용이 따른다. 첫째로, 최적화된 코드를 내버리고 기본 코드로 되돌릴 때 시간이 소모된다. 둘째로, 해당 함수가 여전히 많은 빈도로 호출되고 있다면 JIT는 해당 함수를 다시 최적화 컴파일러에게 보내기로 결정할 수도 있는데, 여기에서 컴파일을 두 번 하는 것에 따른 비용이 발생한다.

웹어셈블리에서는 타입과 같은 것들이 명시적이기 때문에, JIT가 런타임에 데이터를 수집해서 타입을 추론할 필요가 없다. 즉, 재-최적화 단계가 필요없다는 의미이다.

05-06-diagram_compare04-500x201.png

실행

자바스크립트가 빠르게 실행될 수 있도록 작성하는 것은 가능하다. 이를 위해서는 JIT가 어떻게 최적화하는지에 대해서 알고 있어야 한다. 예를 들어 JIT에 관한 예전 글에서 설명했던 것 처럼, 컴파일러가 타입을 특수화(type specialize : 타입이 항상 동일하다고 가정하고 코드를 최적화하는 과정)를 하도록 만드려면 어떻게 코드를 작성해야하는지를 알아야 한다.

하지만, 대부분의 개발자들은 JIT 내부에 대해서 알지 못한다. 심지어 JIT 내부에 대해서 알고 있는 개발자들도, 정확한 개선 포인트를 알기는 쉽지 않다. 코드를 읽기 쉽게 만들기 위해 사용되는 코딩 패턴들(공통 작업을 추상화해서 타입에 관계없이 동작하는 함수로 만드는 작업 등)은 컴파일러가 코드를 최적화하는 것을 방해한다.

게다가 JIT가 사용하는 최적화는 브라우저마다 달라서, 특정 브라우저에 맞추어 코딩을 하면 다른 브라우저에서는 성능이 느릴수도 있다.

이러한 이유로, 웹어셈블리로 작성된 코드를 실행하는 것은 일반적으로 더 빠르다. 자바스크립트를 위해 JIT가 하는 많은 최적화 작업들(타입 특수화와 같은)은 웹어셈블리에서는 전혀 필요없다.

뿐만 아니라 웹어셈블리는 컴파일러를 목표로 디자인되었다. 즉, 인간 프로그래머가 작성하는 용도가 아닌, 컴파일러가 생성해내는 용도로 디자인 되었다는 의미이다.

인간 프로그래머가 직접 프로그래밍할 필요가 없기 때문에, 웹어셈블리는 기계에게 더 이상적인 명령(instruction) 셋을 제공할 수 있다. 코드가 어떤 종류의 작업을 하느냐에 따라 이러한 명령들은 10% 부터 800% 까지 더 빠를 수 있다.

05-07-diagram_compare05-500x171.png

가비지 컬렉션

자바스크립트에서 개발자들은 더이상 필요없는 오래된 변수들을 메모리에서 제거하는 작업에 대해 신경쓸 필요가 없다. 대신에 자바스크립트 엔진이 가비지 컬렉터를 이용해 이러한 작업을 자동으로 해 준다.

하지만 만약 예측가능한 성능을 원한다면, 이는 문제가 될 수도 있다. 가비지 컬렉터가 언제 작동할 지 제어할 수 없기 때문에, 안좋은 타이밍에 실행될 지도 모른다. 대부분의 브라우저들은 이를 잘 스케쥴링해 주고 있지만, 여전히 코드 실행을 방해하는 요인이 될 수도 있다.

최소한 현재까지는, 웹어셈블리는 가비지 컬렉션을 전혀 지원하지 않는다. 메모리는 수동으로 (C나 C++와 같이) 관리된다. 이로 인해 개발자들이 프로그래밍 하기는 더 어려울 수도 있지만, 성능에 있어서는 더 안정된 결과를 만들어낼 수 있을 것이다.

05-08-diagram_compare06-500x204.png

결론

웹어셈블리는 많은 경우에 자바스크립트보다 더 빠르다. 왜냐하면 :

  • 웹어셈블리 코드가 자바스크립트보다 더 간결하기 때문에 (압축된 경우에도 마찬가지), 코드를 가져오는 데에 더 적은 시간이 걸린다.
  • 웹어셈블리 코드를 해독(decode)하는 시간이 자바스크립트의 구문을 해석 (parse) 하는 시간보다 적게 걸린다.
  • 웹어셈블리 코드는 자바스크립트 코드보다 더 머신 코드에 가깝고, 서버단에서 미리 최적화가 되어 있기 때문에 컴파일하고 최적화하는 시간이 적게 걸린다.
  • 웹어셈블리는 타입이나 다른 정보가 미리 내장되어 있기 때문에, 자바스크립트 엔진이 실행시점에 분석할 필요가 없어서 재-최적화하는 시간이 필요없다.
  • 웹어셈블리 코드는 성능을 위해 컴파일러를 느리게 만드는 요인들에 대해 개발자들이 미리 알고 있을 필요가 없으며, 머신에 적합한 명령어 셋을 갖고 있기 때문에 실행시간도 주로 더 적게 걸린다.
  • 메모리를 직접 관리하기 때문에 가비지 컬렉션이 필요없다.

이것이 바로 같은 작업을 할 때 웹어셈블리가 자바스크립트보다 더 좋은 성능을 내는 이유이다.

몇몇 경우에는 기대한 것 만큼 웹어셈블리가 빠르지 않을 때도 있지만, 머지않아 이를 개선할 변화가 있을 것이다. 다음 글에서 이런 내용들을 다루도록 하겠다.

Lin Clark 에 대해

Lin은 모질라 개발자 관계 팀의 엔지니어이다. 그녀는 자바스크립트, 웹어셈블리, Rust, Servo 등을 끄적거리며, 코드 카툰을 그린다.

코드카툰 : http://code-cartoons.com/
트위터 : @linclark

자바스크립트

[번역] 웹어셈블리 모듈의 생성과 동작

이 글은 모질라 Hacks의 웹어셈블리 시리즈 중 네 번째 글인 Creating and working with WebAssembly modules의 번역글이다. 전 시리즈에 걸쳐 웹어셈블리 뿐만 아니라 JIT나 어셈블리 등에 대해서도 체계적으로 잘 설명하고 있고, 카툰까지 곁들여서 이해하기도 쉽다.

총 6부로 되어 있으며, 원작자의 허가를 얻어 전체 시리즈를 모두 번역했으니 처음부터 차례대로 모두 읽어보길 권한다.

  1. 카툰으로 소개하는 웹어셈블리
  2. 저스트-인-타임(JIT) 컴파일러 집중 코스
  3. 어셈블리 집중 코스
  4. 웹어셈블리 모듈의 생성과 동작 (현재글)
  5. 웹어셈블리는 왜 빠를까?
  6. 웹어셈블리의 현재 위치와 미래

 

웹어셈블리는 자바스크립트가 아닌 프로그래밍 언어를 웹페이지에서 실행하기 위한 방법이다. 예전에는 웹페이지 내의 다른 부분들과 상호작용하기 위해 브라우저에서 코드를 실행시키기 위한 유일한 선택지가 자바스크립트였다.

그래서 사람들이 웹어셈블리가 빠르다고 이야기할 때, 유일한 비교대상은 자바스크립트다. 하지만 그것이 웹어셈블리와 자바스크립트 중에서 하나만 선택해서 사용해야 한다는 의미는 아니다.

사실, 우리는 개발자들이 같은 어플리케이션 내에서 웹어셈블리와 자바스크립트를 모두 사용하길 바란다. 심지어 웹어셈블리를 직접 작성하지 않더라도, 그 이점을 누릴 수 있다.

웹어셈블리 모듈은 자바스크립트에서 사용될 수 있는 함수들을 정의한다. 그러므로 오늘날 npm 을 이용해서 lodash와 같은 모듈을 다운로드 받아서 API 에 있는 함수를 호출하듯이, 나중에는 웸어셈블리 모듈을 다운로드 받을 수 있게 될 것이다.

그러면, 웹어셈블리 모듈을 어떻게 생성할 수 있고, 어떻게 자바스크립트에 이용할 수 있는지를 살펴보자.

웹어셈블리는 어디에 어울릴까?

어셈블리에 대해 설명한 글에서, 나는 컴파일러가 상위 레벨 프로그래밍 언어들을 어떻게 읽어들여서 기계 코드로 번역하는지에 대해서 이야기했다.

04-01-langs09-500x306.png

이 그림에서 웹어셈블리는 어떤 역할을 할까?

어쩌면 웹어셈블리가 또 하나의 대상 어셈블리 언어라고 생각할지도 모르겠다. 어느 정도는 사실이지만, 이들 각각의 언어들 (x86, ARM)은 특정한 기계의 아키텍쳐와 연관된다는 점에서 차이가 있다.

사용자의 기계에서 실행될 코드를 웹을 통해서 전달할 때, 당신은 코드가 실행될 대상 아키텍쳐가 무엇인지를 알 수가 없다.

그러므로 웹어셈블리는 다른 종류의 어셈블리와는 약간 다르다. 웹어셈블리는 실제의 물리적인 기계가 아닌 개념상의 기계를 위한 언어이다.

이러한 이유로, 웹어셈블리의 명령(instruction)들은 가끔 가상 명령(virtual instruction)이라고도 불린다. 이들은 자바스크립트 코드보다는 훨씬 더 기계 코드와 직접적으로 맵핑된다. 이들은 공통적으로 널리 쓰이는 하드웨어에서 효과적으로 사용될 수 있는 일종의 교집합을 나타낸다. 하지만 이들이 특정한 하드웨어를 위한 특정 기계 코드에 직접적으로 맵핑되는 것은 아니다.

04-02-langs08-500x326.png

브라우저는 웹어셈블리를 다운로드한다. 그러면 브라우저는 웹어셈블리로부터 대상 기계의 어셈블리 코드를 위한 짧은 홉(hop)을 만들어낼 수 있다.

.wasm 으로 컴파일하기

현재 웹어셈블리를 가장 잘 지원하는 컴파일러 도구 모음(toolchain)은 LLVM이라 불린다. 많은 수의 프론트엔드와 백엔드가 LLVM으로 변환이 가능하다.

Note: 대부분의 웹어셈블리 모듈 개발자들은 C나 Rust 등의 언어들로 개발한 후 웹어셈블리로 컴파일하지만, 웹어셈블리 모듈을 생성하기 위한 다른 방법도 있다. 예를 들면, 타입스크립트를 이용해 웹어셈블리 모듈을 생성할 수 있는 실험적인 도구도 존재하고, 직접 텍스트 형태로 코드를 작성할 수도 있다.

우리가 C를 이용해 웹어셈블리를 작성한다고 가정하자. 우리는 clang 프론트엔드를 이용해서 C를 LLVM 중간 표현 방식으로 변환할 수 있을 것이다. LLVM의 IR로 변환하고 나면, LLVM이 그것을 이해할 수 있으므로 LLVM이 몇가지 최적화를 할 수 있게 된다.

LLVM의 IR(intermidiate representation)에서 웹어셈블리로 변환하기 위해서 우리는 백엔드가 필요하다. 현재 LLVM 프로젝트에서 하나가 진행되고 있다. 이 백엔드는 대부분의 사항이 완료되었고, 곧 완성될 예정이다. 하지만, 지금 당장 사용하기에는 약간 어려울 수도 있다.

지금 당장 사용하기에 좀더 쉬운 Emscripten 이라는 또다른 도구도 있다. 이는 자신만의 백엔드를 갖고 있는데, 이 백엔드는 또다른 대상 (asm.js)으로 컴파일한 후에 그것을 웹어셈블리로 변환하는 방식으로 웹어셈블리를 생성할 수 있다. 하지만 내부적으로는 LLVM을 사용하고 있기 때문에, Emscripten를 이용하면 두개의 백엔드 사이를 전환할 수 있다.

04-03-toolchain07-500x411.png

Emscripten은 전체 C/C++ 코드베이스를 변환할 수 있게 해 주는 많은 추가적인 도구와 라이브러리를 포함하기 때문에, 컴파일러라고 하기 보다는 소프트웨어 개발자 킷 (SDK: Software Developer Kit)에 가깝다. 예를 들어 시스템 개발자들은 주로 쓰기와 읽기를 할 수 있는 파일시스템을 갖고 있는데, Emscripten은 IndexedDB를 이용해서 파일 시스템을 시뮬레이트할 수 있다.

당신이 어떤 도구 모음을 사용했는지와는 관계 없이, 최종 결과는 .wasm 으로 끝나는 파일이 된다. 아래에서 .wasm의 구조에 대해 좀 더 설명하도록 하겠다. 먼저 이것을 자바스크립트에서 어떻게 사용할 수 있는지부터 살펴보자.

자바스크립트에서 .wasm 모듈 로딩하기

.wasm 파일은 웹어셈블리 모듈이며, 자바스크립트에서 로드될 수 있다. 이 시점부터 로딩 과정은 약간 복잡해진다.

function fetchAndInstantiate(url, importObject) {
  return fetch(url).then(response =&amp;amp;amp;amp;amp;amp;gt;
    response.arrayBuffer()
  ).then(bytes =&amp;amp;amp;amp;amp;amp;gt;
    WebAssembly.instantiate(bytes, importObject)
  ).then(results =&amp;amp;amp;amp;amp;amp;gt;
    results.instance
  );
}

여기에 대한 더 깊은 설명은 우리 문서에서 확인할 수 있다.

우리는 이 과정을 좀더 쉽게 만들기 위해 작업하고 있다. 우리는 도구 모음을 좀더 발전시키고, 현존하는 webpack과 같은 모듈 번들러나 SystemJS와 같은 로더들을 통합할 수 있게 되길 기대한다. 우리는 웹어셈블리 모듈을 로딩하는 것이 자바스크립트 모듈을 로딩하는 것만큼 쉬워질 수 있다고 믿는다.

하지만, 웹어셈블리 모듈과 JS 모듈 사이에는 중요한 차이점이 있다. 현재, 웹어셈블리의 함수들은 파라미터나 반환값으로 오직 숫자형(정수 혹은 부동소수)만을 사용할 수 있다.

04-04-memory04-500x93.png

문자열과 같은 더 복잡한 데이터 형을 위해서는, 웹어셈블리 모듈의 메모리를 사용해야만 한다.

만약 당신이 대부분의 작업을 자바스크립트로 해 왔다면, 메모리에 직접 접근하는 것이 그리 익숙하지는 않을 것이다. C나 C++ 혹은 Rust와 같은 더 고성능의 언어들은 주로 메모리를 직접 관리한다. 웹어셈블리 모듈의 메모리는 이러한 언어들에서 찾을 수 있는 힙(heap)을 시뮬레이트한다.

이를 위해서, 웹어셈블리 모듈은 자바스크립트에서 배열 버퍼라 불리는 것을 사용한다. 배열 버퍼는 바이트의 배열이다. 그러므로 배열의 인덱스들은 메모리의 주소의 용도로 사용된다.
당신이 자바스크립트와 웹어셈블리 사이에서 문자열을 전달하려고 한다면, 해당 문자들을 동등한 문자 코드로 변환해야만 한다. 그리고 그 코드를 메모리 배열에 저장한다. 인덱스들이 정수형이기 때문에 인덱스 하나를 웹어셈블리 함수로 전달할 수 있다. 그러므로 해당 문자열의 첫번째 문자에 해당하는 인덱스가 포인터로써 사용될 수 있다.

04-05-memory12-500x400.png

아마 웹개발자들에 의해 사용될 웹어셈블리 모듈을 개발하는 사람은 누구나 해당 모듈 주변에 래퍼를 생성해 줄 것이다. 그렇게 되면 모듈을 사용하는 입장에서는 메모리 관리에 대해 알 필요가 없게 된다.
만약 좀더 배우고 싶다면, 웹어셈블리의 메모리 다루기에 대한 우리의 문서를 확인해보기 바란다.

.wasm 파일의 구조

만약 당신이 상위 레벨 언어로 코드를 작성한 후에 웹어셈블리로 컴파일한다면, 웹어셈블리 모듈이 어떻게 구성되는지에 대해서는 알 필요가 없다. 하지만 기본을 이해하는 것은 도움이 될 것이다.

아직 읽어보지 않았다면, 어셈블리에 대한 글을 읽어보길 권한다. (이 시리즈의 3부)
다음은 웹어셈블리로 변환할 C 함수이다.

int add42(int num) {
  return num + 42;
}

WASM Explorer를 이용하면 이 함수를 컴파일 할 수 있다.

.wasm 파일을 열어보면 (만약 에디터가 지원한다면), 다음과 같은 것을 보게 될 것이다.

00 61 73 6D 0D 00 00 00 01 86 80 80 80 00 01 60
01 7F 01 7F 03 82 80 80 80 00 01 00 04 84 80 80
80 00 01 70 00 00 05 83 80 80 80 00 01 00 01 06
81 80 80 80 00 00 07 96 80 80 80 00 02 06 6D 65
6D 6F 72 79 02 00 09 5F 5A 35 61 64 64 34 32 69
00 00 0A 8D 80 80 80 00 01 87 80 80 80 00 00 20
00 41 2A 6A 0B

이것은 모듈의 “바이너리” 표현 방식이다. 따옴표로 강조한 이유는 보통은 16진법으로 표시되기 때문인데, 이는 쉽게 바이너리 형태나 인간이 읽을 수 있는 형태로 변환이 가능하다.

예를 들어 num + 42 는 다음과 같을 것이다.

04-06-hex_binary_asm01-500x254.png

코드의 작동 방식 : 스택 기계

궁금해할 것 같아서 설명하자면, 이들 명령들은 아래와 같이 동작할 것이다.

04-07-hex_binary_asm02-500x175.png

그림 번역 :
get_local 0 -> 첫번째 파라미터의 값을 읽어서 스택에 추가한다
i32.const 42 -> 상수값을 스택에 추가한다
i32.add -> 스택의 최상위에 있는 두 값을 더해서 결과값을 스택에 추가한다

add 연산이 어디에서 값을 가져와야 하는지에 대해서는 전혀 언급이 없는 것을 눈치챘을지도 모르겠다. 그 이유는 웹어셈블리가 스택 기계라고 불리는 것의 일종이기 때문이다. 이는 한 연산이 실행되기 전에 그 연산이 필요로 하는 모든 값들이 스택에 쌓여 있다는 의미이다.

add와 같은 연산들은 스스로 몇 개의 값이 필요한지를 알고 있다. add는 두 개를 필요로 하기 때문에, 스택의 최상단에서 두 개의 값을 가져올 것이다. 이는 add 명령이 출처나 대상 레지스터를 지정할 필요가 없으므로 short 형(단일 바이트)이 될 수 있다는 것을 의미한다. 이는 .wasm 파일의 사이즈를 줄여주는데, 이로 인해 다운로드에 걸리는 시간도 줄어들게 된다.

비록 웹어셈블리가 스택 기계의 특징을 갖고있긴 하지만, 물리적 기계에서도 똑같이 동작하는 것은 아니다. 브라우저가 웹어셈블리를 자신이 구동되고 있는 기계를 위한 기계 코드로 번역할 때는 레지스터를 사용할 것이다. 하지만 웹어셈블리 코드는 레지스터를 명시하지 않기 때문에, 브라우저로 하여금 해당 기계에 맞는 최선의 레지스터 할당을 할 수 있도록 유연성을 제공해 준다.

모듈의 섹션

.wasm 파일에는 add42 함수 자신 이외에도 다른 부분이 존재한다. 이들을 섹션이라고 한다. 어떤 섹션들은 어떤 모듈에 필수 요건이며, 어떤 섹션들은 선택 요건이다.

필수 요건 :

  1. 타입. 이 모듈에 정의된 함수들이나 임포트한 함수들을 위한 함수 서명(signatures)을 포함한다.
  2. 함수. 이 모듈에 정의된 각 함수에 대한 인덱스를 제공한다.
  3. 코드. 이 모듈의 각 함수에 대한 실제 함수 본문.

선택 요건 :

  1. 익스포트(export). 다른 어셈블리 모듈이나 자바스크립트에서 사용가능한 함수, 메모리, 테이블, 전역 등을 생성한다.
  2. 임포트(import). 다른 어셈블리 모듈이나 자바스크립트에서 임포트한 함수, 메모리, 테이블, 전역 등을 지정한다.
  3. 시작. 웹어셈블리 모듈이 로드되었을 때 자동으로 실행될 함수 (기본적으로 메인 함수와 유사함)
  4. 전역. 모듈을 위한 전역 변수들을 선언한다.
  5. 메모리. 이 모듈이 사용할 메모리를 정의한다.
  6. 테이블. 자바스크립트 객체와 같이 웹어셈블리 모듈 외부에 있는 값들을 맵핑할 수 있도록 해 준다. 이는 간접적으로 함수를 호출할 수 있도록 할 때 특히 유용하다.
  7. 데이터. 임포트된 메모리나 로컬 메모리를 초기화한다.
  8. 요소. 임포트된 테이블이나 로컬 테이블을 초기화한다.

섹션에 대한 추가적인 설명을 원한다면 이들 섹션들이 어떻게 동작하는지에 대한 설명을 참고하길 바란다.

다음 주제

이제 웹어셈블을 어떻게 사용하는지 알았으니, 왜 어셈블리가 빠른지에 대해 알아보자.

Lin Clark 에 대해

Lin은 모질라 개발자 관계 팀의 엔지니어이다. 그녀는 자바스크립트, 웹어셈블리, Rust, Servo 등을 끄적거리며, 코드 카툰을 그린다.

코드카툰 : http://code-cartoons.com/
트위터 : @linclark

자바스크립트

[번역] 어셈블리 집중 코스

이 글은 모질라 Hacks의 웹어셈블리 시리즈 중 세 번째 글인 A crash course in assembly의 번역글이다. 전 시리즈에 걸쳐 웹어셈블리 뿐만 아니라 JIT나 어셈블리 등에 대해서도 체계적으로 잘 설명하고 있고, 카툰까지 곁들여서 이해하기도 쉽다.

총 6부로 되어 있으며, 원작자의 허가를 얻어 전체 시리즈를 모두 번역했으니 처음부터 차례대로 모두 읽어보길 권한다.

  1. 카툰으로 소개하는 웹어셈블리
  2. 저스트-인-타임(JIT) 컴파일러 집중 코스
  3. 어셈블리 집중 코스 (현재글)
  4. 웹어셈블리 모듈의 생성과 동작
  5. 웹어셈블리는 왜 빠를까?
  6. 웹어셈블리의 현재 위치와 미래

 

웹어셈블리가 어떻게 작동하는지를 이해하기 위해서는, 어셈블리가 무엇이고 컴파일러가 어떻게 그것을 생성해내는지를 이해하는 것이 도움이 될 것이다.

JIT에 대해 설명한 글에서 나는 기계와 의사소통을 하는 것이 마치 외계인과 의사소통을 하는 것과 비슷하다고 설명했다.

03-01-alien03-500x286.png

이제 나는 그 외계인의 뇌가 어떻게 작동하는지를 – 즉, 기계의 뇌가 들어오는 정보를 해석하고 이해하는지 – 를 살펴보도록 하겠다.

기계의 뇌에는 생각을 위해 존재하는 부분이 있으며 더하기, 빼기 혹은 논리 연산 등을 수행한다. 또한 그와 가까운 곳에 단기 기억을 제공하는 부분과 장기 기억을 제공하는 부분도 있다.

이들 각각의 부분은 이름을 갖는다.

  • 생각을 담당하는 부분은 산술 논리 장치 (ALU : Arithmetic Logic Unit)이다.
  • 단기 기억은 레지스터에 의해 제공된다.
  • 장기 기억은 랜덤 억세스 메모리 (RAM : Random Access Memory)이다.

03-02-computer_architecture09-500x302.png

기계 코드 내에서의 문장들은 명령(instruction)이라고 불린다.

이러한 명령들 중 하나가 뇌 속으로 들어오면 어떤 일이 벌어질까? 그 명령은 각기 다른 의미를 가진 부분으로 나누어진다.

이 명령이 나누어지는 방식은 뇌의 회로에 의해 특정지어진다.

예를 들어 이런 식으로 구성된 회로를 갖는 뇌는 항상 처음 여섯 비트를 ALU 로 전송할 것이다. ALU는 1과 0의 위치를 기반으로, 두 값을 함께 더해야 한다는 것을 알아낸다.

이 조각은 “opcode” 혹은 연산 코드(operation code)라고 불리는데, 왜냐하면 이것이 ALU에게 어떤 연산을 수행할지를 말해주기 때문이다.

03-03-computer_architecture12-500x354.png

그 후에 뇌는 덧셈을 할 두 개의 수가 무엇인지 알아내기 위해 세 개의 비트로 이루어진 다음 두 조각을 읽어온다. 이들은 레지스터의 주소일 것이다.

03-04-computer_architecture17-500x352.png

기계 코드 위에 있는 주석을 잘 살펴보자. 이를 통해 우리 인간이 기계 코드를 좀더 쉽게 이해할 수 있는데, 이것이 바로 어셈블리이다. 상징적(symbolic) 기계 코드라고도 불린다. 이것이 인간이 기계의 코드를 이해할 수 있는 방법이다.

여기서 어셈블리와 기계 코드 사이에는 꽤 직접적인 관계가 있는 것을 볼 수 있을 것이다. 이 때문에 여러 종류의 기계 아키텍쳐에 맞는 각기 다른 어셈블리가 존재한다. 다른 아키텍쳐의 머신에게는 그 머신에게만 통용되는 별도의 어셈블리가 필요하다.

즉, 번역의 목표 대상이 하나만 있는 것이 아니다. 단순히 하나의 기계 코드가 있는 것이 아니다. 아주 다양한 종류의 기계 코드가 있다. 사람들이 각자 다른 언어로 이야기하듯이, 기계들도 각자 다른 언어로 이야기한다.

인간의 언어를 외계인에게 번역한다면, 아마 영어나 러시아어 혹은 중국어를 외계인 언어 A 혹은 외계인 언어 B로 번역하게 될 것이다. 프로그래밍 용어로 말하자면 C, C++, Rust 등을 x86 이나 ARM으로 번역하는 것이 될 것이다.

이들 상위 레벨 프로그래밍 언어들 중의 하나를 어셈블리 언어들 중 하나로 번역한다고 해 보자. 가능한 한 가지 방법은 각 언어에서 각 어셈블리로 번역할 수 있는 각기 다른 모든 번역기를 만드는 것이다.

03-05-langs05-500x308.png

이 방법은 꽤나 비효율적이다. 이를 해결하기 위해 대부분의 컴파일러는 둘 사이에 최소한 하나의 레이어를 둔다. 컴파일러는 상위 레벨 프로그래밍 언어를 읽어서, 너무 상위 레벨도 아니면서 기계 코드의 레벨에서 동작하지도 않는 무언가로 번역한다. 이것을 중간 표현 형식 (IR: Intermediate representation) 이라고 한다.

03-06-langs06-500x317.png

이 말은 곧 컴파일러가 이들 상위 레벨 언어들 중 어떤 것이든 읽어서 단일 IR 언어로 번역할 수 있다는 의미이다. 여기서부터 컴파일러의 다른 부분이 이 IR을 읽어서 특정 대상의 아키텍쳐에 맞게 컴파일할 수 있게 된다.

컴파일러의 프론트엔드는 상위 레벨 프로그래밍 언어를 IR로 번역한다. 컴파일러의 백엔드는 IR을 대상 아키텍쳐의 어셈블리 코드로 번역한다.

03-07-langs09-500x306.png

결론

지금까지 어셈블리가 무엇이고, 컴파일러가 상위 레벨 프로그래밍 언어를 어떻게 어셈블리로 번역하는지를 살펴보았다. 다음 글에서는 웹어셈블리가 여기에 어떻게 적용되는지를 살펴보겠다.

Lin Clark 에 대해

Lin은 모질라 개발자 관계 팀의 엔지니어이다. 그녀는 자바스크립트, 웹어셈블리, Rust, Servo 등을 끄적거리며, 코드 카툰을 그린다.

코드카툰 : http://code-cartoons.com/
트위터 : @linclark

자바스크립트

[번역] 저스트-인-타임(JIT) 컴파일러 집중 코스

이 글은 모질라 Hacks의 웹어셈블리 시리즈 중 두 번째 글인 A crash course in just-in-time (JIT) compilers의 번역글이다. 전 시리즈에 걸쳐 웹어셈블리 뿐만 아니라 JIT나 어셈블리 등에 대해서도 체계적으로 잘 설명하고 있고, 카툰까지 곁들여서 이해하기도 쉽다.

총 6부로 되어 있으며, 원작자의 허가를 얻어 전체 시리즈를 모두 번역했으니 처음부터 차례대로 모두 읽어보길 권한다.

  1. 카툰으로 소개하는 웹어셈블리
  2. 저스트-인-타임(JIT) 컴파일러 집중 코스 (현재글)
  3. 어셈블리 집중 코스
  4. 웹어셈블리 모듈의 생성과 동작
  5. 웹어셈블리는 왜 빠를까?
  6. 웹어셈블리의 현재 위치와 미래

 

자바스크립트는 태생이 느렸지만, JIT라고 불리는 무언가 덕분에 빨라지게 되었다. 그런데 JIT는 어떻게 동작하는걸까?

자바스크립트는 브라우저에서 어떻게 실행될까

개발자로서 자바스크립트를 페이지에 추가하면, 당신은 목표와 문제를 갖게 된다.

목표 : 당신은 컴퓨터에게 무엇을 할지 말하고 싶다.

문제 : 당신과 컴퓨터는 다른 언어를 사용해서 이야기한다.

당신은 인간의 언어를 사용하고, 컴퓨터는 기계의 언어를 사용한다. 비록 당신이 자바스크립트나 다른 상위레벨 프로그래밍 언어들이 인간의 언어가 아니라고 생각할 지라도, 이들은 실제로 인간의 언어이다. 이들 언어는 기계가 아닌 인간이 이해하기 위해 디자인되었다.

그러므로 자바스크립트 엔진의 임무는 당신이 사용하는 인간 언어를 기계가 이해할 수 있는 무언가로 변경시키는 것이다.

나는 이 상황이 영화 Arrival 에서 인간과 외계인이 서로 대화하려고 하는 장면과 비슷하다고 생각한다.

02-01-alien03-500x286.png

이 영화에서 인간과 외계인은 단지 단어를 단어로 옮기는 식으로 번역하지 않는다. 두 집단은 세상에 대해 생각하는 방식이 다르다. 그리고 이는 인간과 기계 사이에서도 마찬가지이다. (여기에 대해서는 다음 글에서 설명하겠다.)

그러면 어떤 식으로 번역이 이루어질까?

프로그래밍에서 기계 언어로 번역하는 방식에는 일반적으로 2가지가 있다. 바로, 인터프리터와 컴파일러다.

인터프리터를 사용하면, 실시간으로 한줄, 한줄 번역을 진행한다.

02-02-interp02-500x291.png

반면, 컴파일러는 실시간으로 번역하지 않는다. 컴파일러는 미리 작동해서 번역 내용을 생성하고 그것을 적어둔다.

02-03-compile02-500x297.png

이들이 번역하는 방식에는 각각 장점과 단점이 있다.

인터프리터의 장점과 단점

인터프리터는 빠르게 준비해서 실행할 수 있다. 코드를 실행하기 위해 전체 코드를 컴파일하는 단계를 거칠 필요가 없다. 첫번째 줄을 번역하면 바로 실행할 수 있다. 

이 덕분에, 인터프리터는 자바스크립트 같은 언어에 잘 어울리는 것 같다. 웹개발자에게는 시작하자마자 빠르게 코드가 실행되도록 하는 것이 중요하다.

초기 브라우저들이 자바스크립트 인터프리터를 사용한 것은 이러한 이유 때문이다.

동일한 코드를 한번 이상 실행하게 되면 인터프리터의 단점이 발생하기 시작한다. 예를 들어 반복문 내부에 있다고 가정해보자. 그러면 동일한 번역 작업을 계속해서 반복해야만 할 것이다.

컴파일러의 장점과 단점

컴파일러는 정반대의 트레이드-오프를 갖는다.

초기 구동 시간은 조금더 오래 걸리는데, 왜냐하면 처음부터 모든 컴파일 과정을 진행해야 하기 때문이다. 하지만 이후에 코드가 반복해서 실행되면 더 빠른데, 왜냐하면 루프가 진행되는 동안 동일한 번역을 반복할 필요가 없기 때문이다. 

또다른 차이점은 컴파일러가 코드를 좀더 빠르게 실행하기 위해 전체 코드를 둘러보고 편집을 하는 시간이 더 소요된다는 점이다. 이런 편집을 최적화라고 부른다. 

인터프리터는 런타임에 동작하기 때문에, 번역하는 동안에 이러한 최적화를 위해 많은 시간을 소요할 수가 없다.

저스트-인-타임 컴파일러 : 두 세상의 최선

인터프리터의 비효율성 반복문에서 매번 같은 코드를 다시 번역해야 하는 문제 을 제거하기 위해 브라우저는 인터프리터에 컴파일러를 혼합하기 시작했다.

브라우저 마다 방식이 조금씩 다르긴 하지만, 기본 개념은 동일하다. 이들은 자바스크립트 엔진에 모니터(혹은 프로파일러)라고 불리는 부분을 추가했다. 이 모니터는 코드가 실행되는 것을 관찰하면서 해당 코드가 얼마나 많이 실행되고, 어떤 타입이 사용되는지를 기록한다.

먼저, 모니터는 인터프리터를 통해서 모든 것을 실행한다.

02-04-jit02-500x365.png

만약 같은 줄의 코드가 적은 횟수로만 실행되면, 그 코드는 따뜻하다(warm)라고 칭한다. 만약 많이 실행된다면 뜨겁다(hot)라고 칭한다.

기본(Baseline) 컴파일러

어떤 함수가 따뜻해지기(warm) 시작하면 JIT는 그 함수를 컴파일 하기 위해 전송한다. 그 이후에 JIT는 컴파일된 정보를 저장한다.

02-05-jit06-500x368.png

함수의 각 줄은 스텁”으로 컴파일된다. 스텁은 줄번호와 변수 타입에 의해 색인(Index)된다 (이 과정이 왜 중요한지는 나중에 설명하겠다). 만약 동일한 코드가 동일한 변수 타입에 의해 실행되는 것을 모니터가 발견하게 되면, 해당 코드의 컴파일된 버전을 내보낸다.

이 과정은 속도를 향상시켜 준다. 하지만 내가 말했듯이, 컴파일러는 더 많은 일을 할 수 있다. 컴파일러는 최적화를 하기 위해 시간을 들여서 가장 효율적인 방법을 찾아낼 수 있다.

기본 컴파일러가 이러한 최적화의 일부를 담당한다 (아래에서 예제를 하나 보여주겠다). 기본 컴파일러는 너무 많은 시간이 소요되는 것을 원치 않는데, 왜냐하면 실행 시간을 너무 오랫동안 멈추고 싶지 않기 때문이다.

하지만, 만약 코드가 정말로 뜨겁다면(hot) 코드가 아주 많은 횟수로 실행된다면 최적화를 더 하기 위해 추가적인 시간을 들일만한 가치가 있을 것이다.

최적화 컴파일러

코드의 한 부분이 아주 뜨거울(hot) 때, 모니터는 그 부분을 최적화 컴파일러에게 전송한다. 이 컴파일러는 또 다른 더욱 빠른 버전의 함수를 생성하며, 이 함수 역시 저장된다.

02-06-jit09-500x365.png

더 빠른 버전의 코드를 생성하기 위해, 최적화 컴파일러는 몇가지 가정을 해야한다.

예를 들어, 만약 특정한 생성자로부터 만들어지는 모든 객체들이 같은 모양을 갖는다고 가정할 수 있다면 즉 항상 같은 프라퍼티명을 가지며 이들 프라퍼티들이 같은 순서로 추가된다면 이를 이용해서 몇가지 최적화를 할 수 있을 것이다. 

최적화 컴파일러는 이러한 판단을 하기 위해 모니터가 코드의 실행을 지켜보며 수집한 정보들을 사용한다. 이전에 반복문을 순회하는 동안 항상 참이었던 값이 있다면, 그 값이 계속해서 참일 거라고 가정할 수 있을 것이다.

하지만 물론 자바스크립트에서는 어떤 것도 보장할 수 있다. 객체가 99번동안 항상 같은 모양을 하고 있더라도, 100번째에는 프라퍼티 하나가 빠져있을 수도 있다. 

그러므로 컴파일된 코드는 실행하기 전에 가정이 유효한지를 확인해야만 한다. 유효하다면, 컴파일러는 코드를 실행한다. 하지만 유효하지 않다면 JIT는 가정이 잘못되었다고 판단하고 최적화된 코드를 버린다.

02-07-jit11-500x361.png

그러면 실행은 다시 인터프리터 혹은 기본 컴파일된 버전으로 돌아간다. 이 과정은 역최적화(deoptimization) 혹은 구제(bailing out)라고 한다. 

최적화 컴파일러는 주로 코드를 빠르게 만들지만, 가끔 예상하지 못한 성능 문제를 야기하기도 한다. 만약 코드가 계속해서 최적화되고 역최적화된다면, 단순히 기본 컴파일된 버전을 실행시키는 것 보다 느려지게 될 것이다.

대부분의 브라우저들은 이러한 최적화/역최적화 싸이클이 발생했을 때 중지할 수 있는 제한을 갖고 있다. 예를들어 만약 JIT가 10번의 이상의 최적화를 시도하고 계속해서 그 코드를 버린다면, 더이상 최적화를 그만 시도하게 될 것이다.

최적화 예제 : 타입 특수화

최적화에는 다양한 종류가 있는데, 어떻게 최적화가 발생하는지를 보여주기 위해 한가지 형태를 살펴보도록 하겠다. 최적화 컴파일러의 가장 중요한 성과들 중 하나는 타입 특수화라고 불리는 것에서 얻어진다.

자바스크립트가 사용하는 동적 타입 시스템은 실행시점에 약간의 추가 작업이 요구된다. 예를 들어 다음의 코드를 살펴보자.

function arraySum(arr) {
  var sum = 0;
  for (var i = 0; i < arr.length; i++) {
    sum += arr[i];
  }
}

위의 반복문 내부에 있는 += 연산은 간단해 보일 수도 있다. 이 연산은 한번에 할 수 있을것 처럼 보이지만, 동적 타입 때문에 예상했던 것 보다 더 많은 단계가 필요하다.

arr이 100개의 정수를 갖는 배열이라고 가정해보자. 코드가 사용되기 시작하면 기본 컴파일러는 함수안에 있는 각각의 연산에 대한 스텁을 생성할 것이다. 그래서 sum += arr[i]에 대한 스텁이 생성되며, 이 스텁이 += 연산을 정수 연산으로써 처리하게 된다.

하지만 sumarr[i]이 정수라고 보장할 수는 없다. 자바스크립트에서는 타입이 동적이기 때문에, 루프를 다음번에 순회할 때에는 arr[i]가 문자열일 수도 있다. 정수를 더하는 것과 문자열을 연결하는 것은 아주 다른 두 개의 연산이므로, 둘은 아주 다른 기계 코드로 컴파일될 것이다.

JIT는 여러개의 기본 스텁을 컴파일함으로써 이런 상황을 처리한다. 만약 코드 조각이 동일한 구조라면 (즉, 항상 같은 타입들로 호출된다면) 하나의 스텁만 가질 것이다. 만약 코드 조각이 다형 구조라면 (코드 이곳 저곳에서 각각 다른 타입으로 호출된다면), 해당 연산이 실행되는 각각의 타입 조합 만큼 스텁을 갖게 될 것이다.

이 말은 JIT가 스텁을 선택하기 전에 많은 질문을 던져야 한다는 의미이다.

02-08-decision_tree01-500x257.png

각 라인별 코드들이 각자의 스텁 묶음을 기본 컴파일러 내부에 갖고 있기 때문에, JIT는 각 라인의 코드가 실행될 때마다 타입을 확인해야만 한다. 매번 루프를 순회할 때마다, JIT는 같은 질문을 해야만 한다.

02-09-jit_loop02-500x323.png

만약 JIT가 이렇게 반복해서 확인할 필요가 없다면, 코드 실행이 훨씬 빨라질 수 있을 것이다. 이것이 바로 최적화 컴파일러가 하는 일 중 하나이다. 

최적화 컴파일러에서는 전체 함수가 함께 컴파일된다. 타입 확인은 루프 이전에 발생할 수 있도록 이동된다.02-10-jit_loop02-500x318.png

어떤 JIT는 여기서 더 최적화하기도 한다. 예를 들어, 파이어폭스에는 오직 정수만을 갖는 배열들을 위한 특수한 분류가 있다. 만약 arr이 이러한 배열들 중 하나라면, JIT는 arr[i]가 정수인지 확인할 필요가 없게 된다. 이는 JIT가 루프에 진입하기 전에 모든 타입을 확인할 수 있다는 의미이다.

결론

지금까지 JIT에 대해 간략하게 살펴보았다. JIT는 코드가 실행되는 것을 관찰하여 자주 실행되는 코드가 최적화될 수 있도록 전송하여 자바스크립트를 더 빠르게 실행되도록 만든다. 이는 대부분의 자바스크립트 어플리케이션에의 성능을 몇배로 향상시켰다.

하지만 이러한 향상에도 불구하고, 자바스크립트의 성능은 예측하기 어려울 수 있다. 또한 코드를 빠르게 만들기 위해서 JIT는 실행시점에 몇가지 오버헤드를 발생시키는데, 이는 다음을 포함한다:

  • 최적화와 역최적화
  • 모니터의 기록 영역과 코드가 버려질 때 복구를 위한 정보가 사용하는 메모리
  • 기본 버전과 최적화 버전의 함수를 저장하기 위한 메모리

여기에 좀더 개선할 여지가 있다: 이러한 오버헤드는 제거될 수 있다면, 성능이 좀더 예측 가능해진다. 이것이 바로 웹어셈블리가 하는 일 중의 하나이다.

다음 글에서는 어셈블리에 대해, 그리고 컴파일러가 어떤 식으로 함께 동작하는지에 대해서 설명하도록 하겠다.

Lin Clark 에 대해

Lin은 모질라 개발자 관계 팀의 엔지니어이다. 그녀는 자바스크립트, 웹어셈블리, Rust, Servo 등을 끄적거리며, 코드 카툰을 그린다.

코드카툰 : http://code-cartoons.com/
트위터 : @linclark