사용자 시간 API

웹 앱 이해하기

Alex Danilo

고성능 웹 애플리케이션은 뛰어난 사용자 환경을 제공하는 데 필수적입니다. 웹 애플리케이션이 점점 복잡해짐에 따라 매력적인 환경을 만들려면 성능 영향을 이해하는 것이 중요합니다. 지난 몇 년 동안 네트워크 성능, 로드 시간 등을 분석하는 데 도움이 되는 다양한 API가 브라우저에 많았지만, 이들이 애플리케이션 속도를 저하시키는 요인을 충분히 유연하게 찾을 수 있을 정도로 세밀한 세부 정보를 제공하지는 않습니다. User Timing API를 입력하세요. 이 API는 웹 애플리케이션을 계측하여 애플리케이션에서 시간을 보내는 위치를 파악하는 데 사용할 수 있는 메커니즘을 제공합니다. 이 도움말에서는 API와 사용 방법의 예를 살펴봅니다.

측정할 수 없는 것은 최적화할 수 없음

느린 웹 애플리케이션의 속도를 높이기 위한 첫 번째 단계는 시간을 어디에 소비하는지 알아내는 것입니다. JavaScript 코드 영역의 시간 영향을 측정하는 것은 핫스팟을 식별하는 이상적인 방법이며, 이는 성능 개선 방법을 찾는 첫 번째 단계입니다. 다행히 User Timing API는 JavaScript의 여러 부분에 API 호출을 삽입한 다음 최적화에 도움이 되는 자세한 타이밍 데이터를 추출할 수 있는 방법을 제공합니다.

고해상도 시간 및 now()

정확한 시간 측정의 핵심은 정밀도입니다. 예전에는 밀리초 단위로 측정한 타이밍을 기준으로 하는 것이 괜찮았지만, 버벅거림이 없는 60FPS 사이트를 빌드하려면 각 프레임을 16ms 이내에 그려야 했습니다. 따라서 정확도가 밀리초 수준일 경우 좋은 분석에 필요한 정밀도는 부족합니다. 최신 브라우저에 내장된 새로운 시간 유형인 고해상도 시간을 입력합니다. 고해상도 시간은 부동 소수점 타임 스탬프를 제공하며, 이는 마이크로초 단위의 해상도로 이전보다 1,000배 이상 향상되었습니다.

웹 애플리케이션에서 현재 시간을 가져오려면 성능 인터페이스의 확장 프로그램을 구성하는 now() 메서드를 호출합니다. 다음 코드는 그 방법을 보여줍니다.

var myTime = window.performance.now();

PerformanceTiming이라는 다른 인터페이스가 있습니다. 이 인터페이스는 웹 애플리케이션이 로드되는 방식과 관련된 여러 시간을 제공합니다. now() 메서드는 PerformanceTimingnavigationStart 시간이 발생한 시점부터 경과된 시간을 반환합니다.

DOMHighResTimeStamp 유형

과거의 웹 애플리케이션 시간을 측정하려면 DOMTimeStamp를 반환하는 Date.now()와 같은 함수를 사용했습니다. DOMTimeStamp는 밀리초 단위의 정수 값을 값으로 반환합니다. 고해상도 시간에 필요한 정확성을 높이기 위해 DOMHighResTimeStamp라는 새로운 유형이 도입되었습니다. 이 유형은 시간을 밀리초 단위로 반환하는 부동 소수점 값입니다. 하지만 부동 소수점이므로 값은 밀리초 단위까지 표시할 수 있으므로 1,000분의 1의 정확도를 얻을 수 있습니다.

사용자 시간 인터페이스

이제 고해상도 타임스탬프가 준비되었으므로 User Timing 인터페이스를 사용하여 타이밍 정보를 추출해 보겠습니다.

User Timing 인터페이스는 애플리케이션의 여러 위치에서 메서드를 호출할 수 있는 함수를 제공합니다. 이 함수는 헨젤과 그레텔 스타일의 탐색경로 트레일을 제공하여 시간이 어디에서 소비되는지 추적할 수 있습니다.

mark() 사용

mark() 메서드는 타이밍 분석 도구 키트의 기본 도구입니다. mark()는 타임스탬프를 저장합니다. mark()의 매우 유용한 점은 타임스탬프 이름을 지정할 수 있다는 것입니다. 그러면 API가 이름과 타임스탬프를 단일 단위로 기억합니다.

애플리케이션의 여러 위치에서 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');

measure()를 호출하면 설정한 표시와 별개로 결과가 저장되므로 나중에 검색할 수 있습니다. 애플리케이션이 실행되는 동안 남은 시간을 저장함으로써 애플리케이션의 응답성을 유지하고 애플리케이션이 일부 작업을 완료한 후에 모든 데이터를 덤프하여 나중에 분석할 수 있습니다.

clearMarks()로 표시 삭제

설정해 놓은 여러 표시를 제거하는 것이 유용한 경우도 있습니다. 예를 들어 웹 애플리케이션에서 일괄 실행을 수행하여 각 실행을 새로 시작할 수 있습니다.

clearMarks()를 호출하면 설정한 표시를 쉽게 삭제할 수 있습니다.

따라서 아래 예시 코드에서는 기존의 모든 표시를 날려 버립니다. 따라서 원하는 경우 타이밍 실행을 다시 설정할 수 있습니다.

window.performance.clearMarks();

물론 모든 표시를 지우고 싶지 않은 시나리오도 있습니다. 따라서 특정 표시를 제거하려면 삭제하려는 표시의 이름을 전달하면 됩니다. 예를 들어 아래 코드는 다음과 같습니다.

window.peformance.clearMarks('mark_fully_loaded');

첫 번째 예에서 설정한 표시를 제거하고 설정한 다른 표시는 변경하지 않고 그대로 둡니다.

직접 실행한 측정도 삭제하는 것이 좋습니다. 이 경우 clearMeasures()라는 메서드가 있습니다. clearMarks()와 정확히 동일하게 작동하지만 대신 사용자가 측정한 모든 작업을 수행합니다. 예를 들면 다음과 같습니다.

window.performance.clearMeasures('measure_load_from_dom');

measure() 예에서 실행한 측정을 삭제합니다. 모든 측정을 삭제하려면 인수 없이 clearMeasures()를 호출하기만 하면 된다는 점에서 clearMarks()와 동일하게 작동합니다.

타임아웃 데이터 가져오기

표시를 설정하고 간격을 측정하는 것이 좋지만 언젠가는 분석하기 위해 해당 타이밍 데이터를 얻고 싶을 수 있습니다. 이 작업도 정말 간단합니다. PerformanceTimeline 인터페이스만 사용하면 됩니다.

예를 들어 getEntriesByType() 메서드를 사용하면 모든 표시 시간 또는 모든 측정 타임아웃을 목록으로 가져와 반복하고 데이터를 다이제스트할 수 있습니다. 좋은 점은 목록이 시간순으로 반환되므로 웹 애플리케이션에서 조회된 순서대로 표시를 볼 수 있다는 것입니다.

아래 코드는 다음과 같습니다.

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

는 웹 애플리케이션에서 적중된 모든 마크의 목록을 반환합니다. 단, 코드는 다음과 같습니다.

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

우리가 취한 모든 조치의 목록이 반환됩니다.

지정한 특정 이름을 사용하여 항목 목록을 다시 가져올 수도 있습니다. 예를 들어 코드는 다음과 같습니다.

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

그러면 startTime 속성에 'mark_wide_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 reqCnt = 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를 사용해 보세요. 속도를 높이는 방법을 알게 될 것입니다. 사용자는 사용 환경을 크게 개선해준 것에 대해 감사할 것입니다.