자바스크립트 튜닝을 통한 사용자 체감속도 높이기

2013. 11. 14. 15:57 Posted by 초절정고수


퍼옴 : http://html5experts.kr/archives/1481


207-js

이번에 설명하는 것은 자바스크립트를 고속화하는 방법을 정리해봅니다. 자바스크립트 속도라는 관점에서 시작하면 크게 2가지 방법이 존재합니다. 첫번쨰, 실행되는 자바스크립트 코드를 최적화하여 별도의 코드를 고속화하는 것입니다. 병목현상이 있는 코드가 있다면 효과가 높을 수 있지만, 몇 ms라는 시간이 누적되어 느껴지는 최적화형태입니다.

두번째는 사용자가 느끼는 체감속도를 향상시키는 방법으로 자바스크립트 실행속도 자체는 크게 변경하지 않고,사용자 관점에서 보면 페이지가 빨리 표시되는 느낌이 나도록 하는 방법입니다. 이 방법은 약간의 코드 수정과 기본 설계변경만으로 개선할 수 있습니다. 이번에 알려드리는 팁은 사용자가 느끼는 속도감을 처리하는 방법으로 크게 다음과 같이 나눌 수 있습니다.

  • 페이지를 빠르게 본다
  • 사용자에게 빠르게 상호작용으로 발생한 결과를 리턴한다.
  • 사용자와의 상호작용을 낮추는 것을 제외한다.
  • 백그라운드에서 자바스크립트를 실행하는 Web Workers를 이용하자!

페이지를 빠르게 본다.

당연한 말이지만, 페이지를 빠르게 표시한다는 것은 정말로 중요합니다. 첫페이지를 접속할 때 게속 흰색 화면이 그대로 보이는 경우와 조금씩 내용이 표시되는 경우는 사용자가 느껴지는 체감속도가 크게 다르다고 봅니다. 이때 자바스크립트 코드가 크게 영향을 주고 있습니다.

<!doctype html>

<html lang=”ko”>

<head>

<script src=”1.js></script>

<script src=”2.jpg”></script>

</head>

<body>

내용

</body>

</html>

이 예제는 head요소에 script요소에서 2개의 자바스크립트 파일을 읽어드립니다. 이런 경우 자바스크립트 실행이 종료될 때까지 화면을 렌더링하지 않기 때문에 자바스크립트 실행시간이 오래걸리면 그만큼 아무런 콘텐츠가 표시되지 않게 됩니다. 즉, 자바스크립트 실행시간이 걸리면 그만큼 콘텐츠가 표시되지 않고 시간이 걸립니다.

그래서 보통 해결하는 방법이 아래와 같은 방법으로 자바스크립트를 선언합니다.

<!doctype html>

<html lang=”ko”>

<head>

</head>

<body>

내용

<script src=”1.js></script>

<script src=”2.jpg”></script>

</body>

</html>

보통 이 방법을 자주 추천하지만, 이렇게 body요소의 마지막에 script를 추가하면 우선적으로 볼 수 있는 콘텐츠를 표시하고 콘텐츠 표시가 끝난후 자바스크립트가 실행됩니다. 이제 해당 페이지를 바로 볼 수 있게 되어 사용자가 느끼는 체감속도는 빨라질 수 있지만 사용자가 행위를 통해 버튼을 클릭하거나하면 반응하지 않기 때문에 페이지 오류로 인식할 수 있다는 것도 가정해야 합니다.

다른 방법으로는 script요소에 defer속성과 HTML5부터 추가된 async속성을 이용하는 방법도 있습니다. 위에서 작성한 코드와 동일한 효과를 발휘합니다.

<!doctype html>

<html lang=”ko”>

<head>

<script src=”1.js” defer></script>

<script src=”2.jpg” defer></script>

</head>

<body>

내용

</body>

</html>

defer속성을 사용하면 콘텐츠 보기가 완료된 뒤에 자바스크립트를 실행할 수 있습니다. 즉 body요소의 마지막에 script요소를 선언한 것과 동일한 결과가 나오지만, 파일읽기는 비동기적으로 처리되어 약간은 빠르게 처리됩니다. 이 defer속성은 최근 출시된 모던 브라우저에서는 모두 사용할 수 있습니다.

다른 속성인 async속성은 자바스크립트 실행페이지의 표시를 차단하지 않고 자바스크립트를 비동기적으로 실행할 수 있는방법으로 이 속성을 사용하면 스크립트 다음 내용이 표시되면서 자바스크립트 파일 다운로드가 끝나는대로 해당 자바스크립트가 실행됩니다. 단, async속성을 붙인 script요소는 실행순서를 보장하지 않기 때문에 다른 자바스크립트와의 종속성이 없는 것에 한해서 사용해야 합니다. 또한 실행 타이밍도 보장되지 않기 ㅣ때문에 읽혀지지 않은 요소에 대한 DOM조작을 버릴 가능성도 있어 주의해야 합니다.

<!doctype html>

<html lang=”ko”>

<head>

<script src=”1.js” async></script>

<script src=”2.jpg” async></script>

</head>

<body>

내용

</body>

</html>

async속성은 모든 모던 브라우저에서 이용할 수 있지만 IE의 경우 IE9이하는 지원되지 않기 때문에 선언되어도 사용하지 못합니다. 주의해야할 부분은 async속성과 defer속성을 붙인 script요소는 document.write()메소드를 사용할 수 없다는 것으로 인라인에서 자바스크립트를 사용할 수 없다는 것입니다.

 

사용자에게 빠르게 상호작용으로 발생한 결과를 리턴한다.

사용자가 느낄 수 있는 체감속도를 향상시키기 위해서는 사용자 행동에 대해 빠르게 특정 상호작용을 리턴하는 것이 중요합니다. 예로 gmail의 메일 쓰레드에 별표시를 하는 인터페이스를 살펴봅시다.

gmail

 

gmail은 메일 스레드에 빈 별표시를 클릭하는 순간 노란 별로 변경됩니다. 실제로 별을 표시하는 작업은 백그라운드에서 비동기적으로 실행됩니다. 따라서 다른 작업에 영향이 적다면 빠르게 응답을 표시하여 앱이 빠르게 실행되고 있다는 것을 보여줄 수 있습니다. 위와 같이 사용자 행동에 대해 먼저 결과를 표시하는 것만으로도 처리에 대한 시간은 걸리ㅣ지만 진행 표시줄이나 아이콘을 보여주고 사용자에게는 실시간으로 상호작용을 리턴하는 알맞은 예라고 생각됩니다.이는 사용자에게 아무것도 응답이 없다는상태를 최대한 줄이는 것으로 기본적인 디자인에 대한 부분으로 use case에 맞게 생각해야 합니다.

 

사용자와의 상호작용을 낮추는 것을 제외한다.

자바스크립트에 의한 처리가 무거워진다면 화면 업데이트가 되지 않고 사용자 작업이 멈춰버리는 경우가 많아지비니다. 멈춰있는 시간이 너무 길다면 브라우저에서 응답이 없다는 경고도 나올 수 있습니다.(특히 한국에서는 ActiveX가 많이 설치되는 사이트일 수록 이런 문제가 심각하게 발생함) 그렇게 심하지 않았지만 페이지 스크롤중에 멈추거나 애니메이션 화면이 갈라지는 경우가를 경험해보앗을 것입니다. 이런 상태는 브라우저의 사용자 인터페이스 주위 처리와 자바스크립트가 동일한 단일 스레드에서 실행되는 것이 원인입니다.  이런 경우 우선적으로 단일스레드와 이벤트루프라는 구조를 이해해야 합니다.

- 단일스레드와 이벤트 루프

일반적인 브라우저는 단일스레드로 실행하여 자바스크립트 코드와 화면을 렌더링하는 사용자작업(페이지 스크롤, 마우스작업등)을 동시에 처리할 수 없습니다. 따라서 자바스크립트 처리에 시간이 걸리면 그 사이에는 화면을 렌더링하거나 사용자 동작도 멈추게 됩니다. 이런 작업은 이벤트 루프라는 구조로 실행됩니다.

이벤트 루프는 이벤트를 모니터잉하는 무한 루프를 가지고 있는 이벤트(또는 콜백)이 발생하면 발생한 순서대로 해당 이벤트를 처리합니다. 여기서 이야기하는 이벤트는 자바스크립트 이벤트뿐만 아니라, 브라우저 레이아웃 이벤트와 렌더링 이벤트등도 포함됩니다. 따라서 자바스크립트 실행시간이 걸리면 그 사이에 발생하는 렌더링 이벤트와 같은 다른 이벤트가 실행큐에 등록되어 언제까지 처리되지 않는다는 것입니다. 근본적인 대책은 각 프로세스를 단축해야 하지만 이벤트루프 구조를 잘 이용하면 큰 작업을 여러 조각으로 나누어 처리할 수 있습니다.

- setTimeout함수에 의한 의사적인 병렬처리

이벤트루프를 감안하고 자바스크립트처리에 시간이 걸린다면, 그 처리를 종료시키고 다른 이벤트에서 실행하여 처리를 분할할 수 있습니다. 이 분할 처리 도중에 브라우저의 렌더링 이벤트가 발생하면 정상적으로 화면이 업데이트되는 것입니다. 예로 자바스크립트에서 시간이 걸리는 처리를 하기 전에 먼저 외관을 업데이트하는 방법이 있지만, 다음과 같은 코드에서는 화면이 바로 업데이트되지 않고 자바스크립트 실행이 끝날 때까지 리턴되지 않습니다.

var output = document.querySelector(‘#output’); // id속성에 output으로 지정된 요소에 메시지를 표시

output.textContent = ‘메시지’;

// 시간이 오래걸리는 작업

이 메시지가 바로 화면에 반영되고 나서 시간이 걸리는 처리를 하도록 변경하면 아래와 같습니다.

var output = document.querySelector(‘#output’); // id속성에 output으로 지정된 요소에 메시지를 표시

output.textContent = ‘메시지’;

setTimeout(function() {

// 시간이 오래걸리는 작업

}, 0);

setTimeout함수를 이용하여 시간이 많이 걸리는 작업을 처리합니다. setTimeout함수는 일정시간후 지정된 메소드를 실행하는 함수로 여기에서는 0ms후로 지정합니다. 이는 실제로 바로 실행되는 것이 아니기 때문에 이벤트루프의 실행큐를 등록될 수 있습니다. 따라서 보려는 메시지의 렌더링 이벤트가 우선 실행되는 것이 아니라 이벤트루프 큐에 등록될 수 있습니다. 즉, 메시지의 렌더링 이벤트가 우선 실행된 다음에 setTimeout 함수에 지정된 작업이 실행됩니다.

이렇게 setTimeout함수를 사용하여 명시적으로 큰 작업을 분할하고 의사적인 병렬처리를 할 수 있게 됩니다. 단 너무 많이 사용하면 코드의 가독성이나 관리적인 부분이 떨어지기 때문에 주의해야 합니다. 그리고 setTimeout함수이외에도 이벤트 및 콜백하면 좋기 때문에 XMLHttpRequest 및 기타 이벤트 처리도 같은 방법입니다. 자바스크립트는 원래 그런 내용이 많아서 의도하지 않아도 그런  코드가 잇다고 생각합니다. 그러나 이런 구조를 알아야 문제도 해결할 수 있기에 기억해두면 좋습니다.

현재 setTimeout함수를 사용하여 특정 작업을 실행 큐 긑에 스택하는 방법은 setImmediate함수로 표준화가 논의되고 있다고 합니다. 현재 IE9이상에서는 구현되어 있습니다.

 

백그라운드에서 자바스크립트를 실행하는 Web Workers를 이용하자!

HTML5에서는 setTimeout함수를 사용한 의사적인 병렬처리가 아닌 진짜 병렬처리를 수행하는 Web Workers를 지원합니다. 이 기능은 자바스크립트 처리를 브라우저의 메인스레드와 별도로 백그라운드에서 처리할 수 있습니다. 따라서, 화면 그리기 및 사용자 작업을 방해하지 않고 시간이 걸리는 자바스크립트를 실행할 수 있습니다.

Web Workers를 사용하면 백그라운드에서 처리하는 자바스크립트를 별도로 설정해야 합니다. 예제도 웹페이지에서 읽혀지는 main.js와 백그라운드에서 실행되는 worker.js로 나뉘어져 있습니다.

var worker = new Worker(‘worker.js’);

worker.addEventListner(‘message’, function(e) {

console.log(e.data);

});

var message = ‘테스트’;

worker.posotMessage(message);

main.js는 백그라운드에서 실행하는 Worker객체를 인수 worker.js를 사용하여 만듭니다. 그리고 작업자에게 데이터 송수신으로 서로 메시지를 주고받습니다. 작업자의 postMessage()메소드 가공 데이터를 전송하고 message 이벤트에서 작업자에게 리턴된 데이터를 전송합니다. 송수신할 수 있는 데이터는 기본 형식 및 개체등이 가능하지만 Error개체 및 함수(Function) DOM노드등은 오류입니다.

self.addEventListener(‘message’, function(e) {

//메시지 수신하여 그대로 리턴

self.postMessage(e.data);

}):

worker.js는 main.js와 같이 message이벤트와 postMessage()메소드를 사용하여 메시지를 교환합니다. 여기서 message이벤트에서 데이터를 수신받은 데이터를 postMessage()메소드를 그대로 리턴합니다. 예로 어떤 작업을 할 때 여기서 데이터를 가공하여 리턴하는 것 처럼 변경하면 좋을 것입니다. 작업자 자신에 접근하려면 self변수를 선언합니다. 또한 작업자 내에서 window개체 또는 페이지 DOM트리에 접근할 수 없기 때문에 주의해야 합니다.

이렇게 main.js는 기본적으로 사용자 인터페이스 작성 및 작업자 작업결과를 반영하기에 쉽고 작업자의 실제작업을 백그라운드에서 할 수 있도록 사용자 작업을 방해하지 않도록 같은 만들기 포함이 가능합니다. 또한, Web Workers를 사용하여 비즈니스 로직등의 코드를 추가하여 MVC모델과 같은 구성을 취하는 것도 쉽습니다.

- Web Workers 대응 상태

데스크톱의 경우 IE9를 제외한 최근 모든 브라우저에서 사용할 수 있습니다. 모바일은 iOS는 지원하지만, Android는 지원되지 않고 있어 fakeworker-js등의 polyfill을 이용합니다.

메시지 교환 데이터는 Boolean, Number, String, Date, RegExp, ImageData, File, Blob, FileList, Array, Object개체도 가능합니다. 반대로 처리할 수 없는 데이터는 Error, Function등의 개체와 DOM노드 RegExp개체의 lastIndex속성등입니다. 그리고 setter 및 getter, prototype등은 복사되지 않습니다.

작업자는 DOM트리, window개체, document개체, parent개체에 접근할 수 없습니다. 작업자에서 사용할 수 있는 기능으로는 navigator 개체 및 location개체(읽기용), XMLHttpRequest, setTimeout과 setInterval함수의 Application Cache입니다. 또한 다른 작업자를 만들 수 있고 작업자에서 사용할 수 있는 고유 기능은 외부스크립트를 가져올 수 있는 importScripts함수등이 있습니다.