User Timing API

Understanding your Web App

HTML5 Rocks

고성능 웹 애플리케이션은 훌륭한 사용자 경험(UX)에 대단히 중요합니다. 웹 애플리케이션이 더욱 복잡해지면서 성능에 미치는 영향을 이해하는 것은 강렬한 경험을 위해 필수입니다. 지난 몇 년 동안 수많은 API가 브라우저에 나타나서 네트워크 성능, 로딩 타임 등의 분석에 도움을 주었지만, 애플리케이션을 느리게 하는 것이 무엇인지 찾을만한 융통성과 세부 내용을 제공하지는 못했습니다. User Timing API를 확인해 보세요. 애플리케이션이 시간을 소모하는 곳을 알아내기 위하여 애플리케이션에 설치해 사용할 수 있는 메커니즘을 제공합니다. 이 글은 API와 그 사용법에 대한 예제를 다룹니다.

측정할 수 없으면 최적화할 수 없습니다

느린 웹 애플리케이션의 속도를 향상하는 첫 번째 단계는 시간을 소모하는 부분을 해결하는 것입니다. JavaScript 코드 영역에서 시간이 미치는 영향 측정하기는 핫스팟을 알아내는 이상적인 방법이며, 성능 향상법을 찾는 첫 단계입니다. 운 좋게도 User Timing API는 JavaScript의 여기저기에 API 호출을 삽입해서 자세한 타이밍 데이터를 추출하는 방법을 제공합니다. 이 데이터를 최적화에 도움을 주는 데 사용할 수 있습니다.

High Resolution time과 ‘now()

정확한 시간 측정의 핵심은 정밀성입니다. 대략적인 밀리 초 측정에 기반을 둔 타이밍(timing, 역자주: 시간 측정하기)이 있던 예전에는 그걸로 충분했습니다. 하지만 멈칫거리지 않는 60 FPS 사이트 개발은 각 프레임을 16ms에 그려야 한다는 것을 의미합니다. 그래서 밀리 초 정확도만으로는 훌륭한 분석에 필요한 정밀성이 부족합니다. 최신 브라우저가 내장하고 있는 새로운 타이밍 타입인 High Resolution Time을 확인해보세요. High Resolution Time은 마이크로초 단위까지 정확히 표현할 수 있는 부동소수점 타임스탬프를 제공하며, 이는 이전보다 천 배 더 정밀합니다.

웹 애플리케이션에서 현재 시간을 얻으려면, 'now()' 메소드를 호출합니다. 이 메소드는 Performance 인터페이스의 extension을 생성합니다. 다음 코드는 그 방법을 보여줍니다.

var myTime = window.performance.now();

웹 애플리케이션을 로드하는 방법에 관련있는 여러 다른 시간을 제공하는 PerformanceTiming이라는 인터페이스가 있습니다. now() 메소드는 PerformanceTiming에서 navigationStart 시간이 발생한 때로부터 걸린 시간을 리턴합니다.

DOMHighResTimeStamp 타입

과거에는 웹 애플리케이션의 시간을 측정하려면, DOMTimeStamp을 리턴하는 Date.now() 같은 것을 이용했습니다. DOMTimeStamp는 밀리초를 정수 값으로 리턴합니다. 고밀도 시간을 위해 필요한 더 좋은 정밀도를 제공하기 위해, DOMHighResTimeStamp라는 새로운 타입을 도입했습니다. 이 타입은 부동소수점 값이며 이것도 밀리초 시간을 리턴합니다. 하지만 부동소수점이므로 아주 작은 밀리초를 표현할 수 있습니다. 그래서 1000분의 1밀리 초 정확도를 나타낼 수 있습니다.

User Timing 인터페이스

이제 고밀도 타임스탬프가 있으니 User Timing 인터페이스를 사용하여 타이밍 정보를 얻어봅시다.

User Timing 인터페이스는 시간을 소모하는 부분 추적을 위해 헨젤과 그레텔 방식인 빵 부스러기 자취를 제공할 수 있는 애플리케이션의 각각 다른 지점에서 메소드를 호출하도록 하는 함수를 제공합니다.

mark()’ 사용하기

mark()’ 메소드는 타이밍 분석 툴킷의 핵심 도구입니다. mark()은 타임스탬프를 저장해줍니다. mark()의 대단히 유용한 점은 타임스탬프에 이름을 붙일 수 있다는 것이며, API는 해당 이름과 타임스탬프를 단일 단위로 기억합니다.

애플리케이션의 다양한 지점에서 mark() 호출하면 웹 애플리케이션에서 그 ‘마크(mark)’에 도달하는데 걸린 시간을 계산해 줍니다.

스펙에서는 수많은 마크를 위해 제안하는 이름을 호출하는데, ‘mark_fully_loaded’, ‘mark_fully_visible’, ‘mark_above_the_fold’ 등 흥미롭고 자명합니다.

예를 들면, 애플리케이션이 다음 코드를 사용하여 완전하게 로딩되면 마크를 설정할 수 있습니다.

window.performance.mark('mark_fully_loaded');

웹 애플리케이션 전체에 걸쳐 이름을 부여한 마크를 설정하여, 수많은 타이밍 데이터를 수집하여 시간이 날 때 분석하여 애플리케이션이 무엇을 언제하는지 계산할 수 있습니다.

measure()’로 측정 계산하기

일단 다수의 타이밍 마크를 설정하면, 그들 간의 소요 시간을 알아내고 싶을 것입니다. ‘measure()’ 메소드를 사용하여 그것을 할 수 있습니다.

measure() 메소드는 마크 간의 소요 시간을 계산합니다. 그리고 마크와 PerformanceTiming 인터페이스의 잘 알려진 이벤트 이름 간의 시간을 측정할 수도 있습니다.

예를 들면, 아래 코드를 사용하여 DOM 완료 시점에서 애플리케이션 상태가 완전히 로딩되는 시간을 계산할 수 있습니다.

window.performance.measure('measure_load_from_dom', 'domComplete', 'mark_fully_loaded');
노트: 이 예제에서 PerformanceTiming 인테페이스의 잘 알려진 ‘domComplete’라는 이름을 넘겨주고 있습니다.

measure()를 호출할 때 설정한 마크와 별도로 결과를 저장하고, 나중에 이를 검색할 수 있습니다. 애플리케이션을 실행할 때 시간을 저장하므로 애플리케이션의 반응성을 유지합니다. 그리고 애플리케이션이 끝나고나서 모든 데이터를 덤프(dump)하여 나중에 분석할 수 있습니다.

clearMarks()’로 마크 버리기

가끔은 설정한 다수의 마크를 제거할 수 있는 것이 유용합니다. 예를 들면, 애플리케이션에서 배치(batch) 실행을 하기 때문에, 각 실행을 새롭게 시작하고 싶어할 수 있습니다.

'clearMarks()'을 호출해서 설정한 마크를 제거하는 것은 충분히 쉽습니다.

아래 예제 코드는 존재하는 모든 마크를 지우므로, 원한다면 타이밍 실행을 다시 설정할 수 있습니다.

window.performance.clearMarks();

물론, 모든 마크 삭제를 원하는 것이 아닌 시나리오가 있습니다. 그래서 특정한 마크를 제거하려면, 단지 지우려는 마크의 이름을 넘기면 됩니다. 예를 들면, 아래의 코드는 첫번째 예제에서 설정한 마크는 제거하고 다른 마크는 그대로 둡니다.

window.peformance.clearMarks('mark_fully_loaded');

측정한 것을 제거하고 싶을 수도 있으며, 이를 하는 'clearMeasures()'라는 상응하는 메소드가 있습니다. clearMarks()와 같이 동작하지만, 대신 이미 행한 측정에 대해서 동작합니다. 예를 들면, 다음 코드는 위의 measure() 예제에서 생성한 측정을 제거합니다.

window.performance.clearMeasures('measure_load_from_dom');

모든 측정을 삭제하고 싶으면, 인자(arguments) 없이 clearMeasures()를 호출하면 clearMarks()와 같이 동작합니다.

타이밍 데이터 얻기

마크를 설정하고 시간 간격을 측정하는 것이 모두 잘 됩니다. 그런데 어느 시점에는 그 모든 데이터를 얻어서 어떤 분석을 하고 싶어집니다. 이것도 정말 간단해서 PerformanceTimeline 인터페이스를 사용하기만 하면 됩니다.

예를 들면, 'getEntriesByType()' 메소드는 모든 마크의 시간 혹은 모든 측정 시간을 목록으로 알려주므로, 그것을 반복하여 데이터를 완전히 이해할 수 있습니다. 좋은 점은 받은 목록이 발생 순서대로여서, 웹 애플리케이션에서 발생한 순서대로 마크를 볼 수 있다는 것입니다.

아래 코드는 웹 애플리케이션에서 도달한 모든 마크의 목록을 리턴합니다.

var items = window.performance.getEntriesByType('mark');

반면에 아래 코드는 측정을 한 목록을 리턴합니다.

var items = window.performance.getEntriesByType('measure');

그것들에 지어준 특정한 이름을 이용하여 엔트리 목록도 얻을 수 있습니다. 그래서 예를 들면, 아래 코드는 startTime 속성에 ‘mark_fully_loaded’ 타임스탬프를 포함하는 하나의 아이템이 있는 목록을 리턴합니다.

var items = window.performance.getEntriesByName('mark_fully_loaded');

XHR 요청 시간 측정하기 (예제)

이제 User Timing API에 대한 적절한 그림을 가지고 있으므로, 웹 애플리케이션에서 XMLHttpRequests가 얼마나 걸리는지 분석하는 데 사용할 수 있습니다.

우선 send() 요청을 수정하여 마크를 설정하는 함수를 생성합니다. 이와 동시에 성공 시 호출하는 콜백을 다른 마크를 설정하여 요청에서 걸린 시간 측정을 만드는 함수 호출로 변경합니다.

보통 XMLHttpRequest는 이런 모습입니다.

var myReq = new XMLHttpRequest();
myReq.open('GET', url, true);
myReq.onload = function(e) {
  do_something(e.responseText);
}
myReq.send();

이 예제를 위해 전역 카운터를 추가하여 요청 수를 기록하고, 이를 사용하여 일어나는 요청마다 측정을 저장합니다. 이것을 하는 코드는 이런 식입니다.

var reqCount = 0;

var myReq = new XMLHttpRequest();
myReq.open('GET', url, true);
myReq.onload = function(e) {
  window.performance.mark('mark_end_xhr');
  reqCnt++;
  window.performance.measure('measure_xhr_' + reqCnt, 'mark_start_xhr', 'mark_end_xhr');
  do_something(e.responseText);
}
window.performance.mark('mark_start_xhr');
myReq.send();

위 코드는 보내는 모든 XMLHttpRequest마다 고유 이름값과 함께 측정을 생성합니다. 요청들을 연속으로 실행한다고 가정하고 있습니다. 병렬 요청을 위한 코드는 순서가 바뀌어 리턴하는 요청을 다루기 위해 조금 더 복잡할 필요가 있으며, 독자를 위한 연습 문제로 남겨두겠습니다.

웹 애플리케이션이 다수의 요청을 처리하면, 아래 코드를 사용하여 콘솔에 모두 덤프할 수 있습니다.

var items = window.performance.getEntriesByType('measure');
for (var i = 0; i < items.length(); ++i) {
  var req = items[i];
  console.log('XHR ' + req.name + ' took ' + req.duration + 'ms');
}

결론

User Timing API가 제공하는 많은 훌륭한 도구를 웹 애플리케이션의 어떤 부분에 적용할 수 있습니다. 웹 애플리케이션 여기저기에 API 호출을 포함하고 시간을 어디에서 소요하고 있는지 명확한 그림을 그릴 수 있는 타이밍 데이터를 후처리하면서 웹 애플리케이션에서 쉽게 핫스팟으로 좁혀나갈 수 있습니다. 하지만 브라우저가 이 API를 지원하지 않는다면 어떻게 할까요? 문제없습니다. API를 정말로 잘 에뮬레이트하고 webpagetest.org와도 잘 동작하는 훌륭한 폴리필(polyfill)이 여기 있습니다. 무엇을 기다리고 있습니까? 지금 User Timing API를 애플리케이션에 사용해보세요. 여러분은 애플리케이션을 빠르게 하는 방법을 알아내게 되고, 사용자들은 그들 경험이 훨씬 좋아진 것에 감사하게 될 것입니다.

Comments

0