자바스크립트 최적화 문법정리


1. 유효범위 관리


이전 글에서 이미 설명했듯이 자바스크립트에서 유효범위를 관리한다는 것은 코드 성능과
매우 밀접한 관계를 맺고 있습니다.


간단히 설명하자면, 자바스크립트의 모든 함수는 유효범위 체인을 가지고 있고,
그 함수 내부의 식별자는 각각의 범위에 맞게 서로 다른 식별자를 담은 객체로 유효범위
체인 내에 그룹을 형성하고 있으며, 모든 식별자 분석 시 유효범위체인 내부에 생성된
객체를 검색하게 됩니다.


또 그 과정을 통해 체인 깊숙이 들어가 있는 식별자를 검색하기 위해서는 그만큼 시간과 비용이 듭니다.

즉, 결론적으로 유효범위를 올바르게 관리하지 못한다면, 그만큼 전체적인 성능저하가 일어날 수 있다는 얘기입니다.


더 자세한 내용은 아래 링크를 참고하시기 바랍니다.



1. 자바스크립트 함수 유효범위(Scope) 관리

2. 자바스크립트 함수 유효범위(Scope) 관리 및 Closer



2. 지역변수에 저장하여 사용하기


이 내용은 앞서 얘기한 유효범위 관리의 내용과 일치되는 내용이 많습니다.


이유는 유효범위체인 내부에 형성된 그룹객체 중 해당 함수의 지역변수들을 담고 있는
활성화 객체는 몇 가지 경우(with, catch의 스코프 확장)를 제외하고는 항시 체인의 가장 처음에 생성되어 있습니다.


그 말은 그만큼 검색시간이 짧아진다는 말과 같으며, 또한 검색시간이 짧아진다는 것은 전체적인 성능향상에 효율적이라는 말과 같습니다.

또한, 이와 같은 현상은 V8 엔진을 사용하는 구글 크롬과 nitro엔진을 사용하는 사파리 4+늘 제외한 모든 브라우저에서 볼 수  있는 현상입니다.


"이 둘은(크롬, 사파리) 자바스크립트 엔진이 너무 빨라서 식별자가 어디에 저장되어 있든 접근 속도에 별다른 영향을 주지 않습니다."


하지만 국내에서 가장 많이 쓰이고 있는 IE 및 그 밖에 대부분의 브라우저에서는 극명한 차이를 느낄 수 있습니다.

"즉, 함수에서 유효범위 밖에 있는 식별자를 두 번 이상 사용할 경우, 그 값을 지역변수에 캐시(정의)하여 사용하는 것이 전체적인 성능향상에 효율적이라는 얘기입니다."


  1. function createElement(){
  2.    
  3.     var createElem = document.createElement('div');
  4.     createElem.id = 'createElem';
  5.     createElem.innerHTML = 'createElem';
  6.    
  7.     document.body.appendChild(createElem);
  8.  
  9.     return document.getElementById('createElem');
  10. }
  11.  
  12. alert(createElement().id); // createElement
  13.  
  14.  
  15. alert((function(doc){
  16.    
  17.     // closer
  18.     return function createElement(){
  19.        
  20.         // 유효범위 체인의 가장 마지막(전역변수객체)에
  21.         // 생성되어 있는 document 전역객체를
  22.         // 해당 함수의 지역변수에 저장하여 식별자 분석시
  23.         // 빠르게 접근할수 있도록 사용하였다.
  24.         var createElem = doc.createElement('div');
  25.         createElem.id = 'createElem';
  26.         createElem.innerHTML = 'createElem';
  27.        
  28.         doc.body.appendChild(createElem);
  29.  
  30.         return doc.getElementById('createElem');
  31.     }
  32. })(document)().id); // createElement
  33.  


대부분의 브라우저에서 지역변수값을 R/W 하는데 드는 비용은 크게 차이 나지 않습니다.

하지만 배열이나 객체는 다릅니다.

이들로부터 값을 R/W 할 때에는 실제로 저장된 위치를 얻어야 하는데 배열은 인덱스를
객체는 해당 객체의 속성 이름을 얻어와야 합니다.


데이터 접근에 드는 비용과 시간은 데이터 구조의 깊이가 늘어남에 커지게 됩니다.

즉, new Object·test_variable_length의 데이터 접근이 newObject·test_variable_length·test_variable_length_1.
test_variable_length_2보다 빠르다는 것이며, 이를 최적화하려는 방법은 이전과 같이 해당 데이터를 지역변수에 캐시(정의) 하여 사용하는 것입니다.



참고로 객체의 속성에 접근하기 위해 자바스크립트에는 두 가지 표현 방법이 존재하는데, 그 방법으로는 점(.) 표기법과 대괄호(['속성']) 표기법이 있습니다.

보통 두 가지 표기법상에 성능 차이는 거의 없지만, nitro 엔진을 사용하는 사파리 등에서
대괄호 표기법을 사용하는 것이 점 표기법을 사용하는 것보다 성능상 좋지 문제점이 있습니다.



  1.     var newObject = {  
  2.         'test_variable_length': new Object().hasOwnProperty.length
  3.     };
  4.      
  5.     var newObject = {  
  6.         'test_variable_length': {
  7.             'test_variable_length_1': {
  8.                 'test_variable_length_2': {}.hasOwnProperty.length
  9.             }
  10.         }
  11.     };
  12.      
  13.      
  14.     // 데이터 접근에 지역변수를 활용하지 않은 예
  15.     (function(doc){
  16.        
  17.         // closer
  18.         return function createPrivateVariable(){
  19.            
  20.             var n = 0;
  21.             for (var i = 0, length = 200000; i < length; i++){
  22.                 for (var k = 0; k < newObject.test_variable_length; k++){
  23.                     n++;
  24.                 }
  25.             }
  26.      
  27.             return n;
  28.         }
  29.     })(document)();
  30.      
  31.      
  32.     // 데이터 접근에 지역변수를 활용한 예
  33.     (function(doc){
  34.        
  35.         // closer
  36.         return function createPrivateVariable(){
  37.            
  38.             var n = 0;
  39.             for (var i = 0, len1 = 200000; i < len1; i++){
  40.                 for (var k = 0, len2 = newObject.test_variable_length.test_variable_length_1.test_variable_length_2; k < len2; k++){
  41.                     n++;
  42.                 }
  43.             }
  44.      
  45.             return n;
  46.         }
  47.     })(document)();
  48.  



3. Html Collection 객체는 반드시 캐시 한다.




자주 사용하는 Html Collection 구성원은 아래와 같습니다.

메서드:

  1. document.getElementsByName()
  2. document.getElementsByClassName()
  3. document.getElementsByTagName()


속성:

  1. document.images
  2. document.links
  3. document.forms
  4. document.forms[0].elements
  5. element·child Nodes / elements.length;


이 요소는 모두 Dom의 최신 상태를 실시간으로 질의 하여 그 결과를 반환해주는 속성과
방법입니다.


보통 DOM에 의한 속성 참조는 비 DOM 속성 참조보다 훨씬 비용이 많이 들며, 그중에서도 HTML Collection 객체를 사용할 때 가장 큽니다.


그러므로 모든 컬렉션 객체를 사용할 때에는 반드시 그 결과를 지역변수에 캐시 하여 사용하는 것이 효율적인 방법입니다.

  1.     // 순회시 데이터 접근이 총 2회씩 일어난다.
  2.     // 1. 컬렉션 전체 길이를 알아오기위해 객체에 접근하고 쿼리하여 해당 결과를
  3.     // 반환한다.
  4.      
  5.     // 2. 전체 div 컬렉션 객체에서 주어진 위치(index)의 객체를 가져온다.
  6.     (function(doc){
  7.        
  8.         // closer
  9.         return function getElementsDiv(){
  10.            
  11.             var n = null;
  12.      
  13.             for (var i = 0; i < document.getElementsByTagName('div').length; i++){
  14.                 n += document.getElementsByTagName('div')[i];
  15.             }
  16.      
  17.             return n;
  18.         }
  19.     })(document)();
  20.      
  21.      
  22.     // 순회시 데이터 접근이 총 1회씩 일어난다.
  23.     // 1. 전체 div 컬렉션 객체에서 주어진 위치(index)의 객체를 가져온다.
  24.     (function(doc){
  25.        
  26.         // closer
  27.         return function getElementsDiv(){
  28.            
  29.             var n = null;
  30.      
  31.             var divs = document.getElementsByTagName('div');
  32.             for (var i = 0, length = divs.length; i < length; i++){
  33.                 n += divs[i];
  34.             }
  35.      
  36.             return n;
  37.         }
  38.     })(document)();
  39.  
  40.  

4. if

if 문은 코드 작성 시 가능한 범위 내에서 우선적으로 가장 많이 발생할 만한 조건식부터
작성하는 것이 효율적입니다.


이유는 확률상 조건문 내부 처리시 조건식에서 빠르게 빠져 나갈 수 있기 때문입니다.

또 다른 방법으로는 조건식 내부에 또다른 조건식 분기를 만드는 방법이 있습니다.

이것으로 실행되는 조건식 수를 줄일수 있습니다.



  1. // 확률상 높은 숫자가 많이 나온다고 가정한 예입니다.
  2.  
  3.  
  4. alert((function(doc){
  5.    
  6.     // closer
  7.     return function getTerms(result){
  8.        
  9.         for (var i = 5; i > 0; i--){
  10.             if (i === result){
  11.                 return i;
  12.             }
  13.         }
  14.     }
  15. })(document)(5));

5. switch

switch문은 if 조건식이 일정량 이상 많아질 경우 사용하게 되며, 보통 조건식이
3개이상일 경우 사용하는 것이 가장 효율적입니다.


또한, 자바스크립트에서는 switch문 대체 할 수 있는 객체 리터널이라는
문법또한 존재합니다.


객체 리터널은 보통 조건식이 많을경우 효율적입니다.

즉, 객체를 통한 방법인지라 데이터 접근이 조금은 느려질수는 있겠지만, 각각의
조건식을 실행하는 비용보다는 훨씬 적게 들기 때문입니다.



  1. var n = '';
  2. var term = 1;
  3. switch (term){
  4.    
  5.     case 1:
  6.     alert(term);
  7.     break;
  8. }
  9.  
  10. // 객체 리터널을 활용한 예
  11. alert({
  12.     1: '1'
  13. }[term]);
  14.  
  1.  


조건식 형태에 따른 문법 정리

1. if문: 조건식이 3개 이하일경우

2. swith문 : 조건식이 3 ~ 10개 이하일경우

3. 객체: 조건식이 10개 이상일경우 및 검색 조건식이 메소드가 아닌 값 형태일 경우.



6. for 문

위에서 자주 언급한 바와 같이 배열의 length를 지역변수에 저장하여 효율적인
코드를 작성합니다.



  1.     alert((function(doc){
  2.        
  3.         // closer
  4.         return function getFors(result){
  5.            
  6.             var a = [1, 2, 3, 4, 5];
  7.             for (var i = 0, length = a.length; i <= length; i++){
  8.                 if (i === result){
  9.                     return i;
  10.                 }
  11.             }
  12.         }
  13.     })(document)(5)); // 5
  14.  
  15.  



두 번째 방법으로 보통 for 문은 카운트를 거꾸로 내려가서 0과 비교하는 것이
배열의 length 또는 0이 아닌 값과 비교하는 것보다 빠릅니다.

  1. alert((function(doc){
  2.    
  3.     // closer
  4.     return function getFors(result){
  5.        
  6.         var a = [1, 2, 3, 4, 5];
  7.         for (var i = a.length; i--; ){
  8.             if (a[i] === result){
  9.                 return i;
  10.                 break;
  11.             }
  12.         }
  13.     }
  14. })(document)(5)); // 4



Object 객체의 프로토타입 맴버를 확장시킨후 해당 객체를 탐색한 결과

  1. // 객체 생성
    var Forin ={
  2.     'test1':1,
  3.     'test2':2,
  4.     'test3':3
  5. };
  6.  
  7. Object.prototype.test4 = 4;// Object Property 확장
  8.  
  9. for (var n in Forin){
  10.    
  11.     alert(n); // test4, test1, test2, test3
  12. }
  13.  
  14.  
  15. // 모든 객체는 위에서 말한것처럼 내장생성자를 확장한 test4 속성이
  16. // 실시간으로 모든 객체에 반영되어 있습니다.
  17.  
  18.  
  19. var obj1 = {};
  20. var obj2 = {};
  21.  
  22. for (var n in obj1){
  23.    
  24.     alert(n); // test4
  25. }
  26.  
  27. for (var n in obj2){
  28.    
  29.     alert(n); // test4
  30. }
  31.  
  32. // 확장된 속성을 가리기 위하여 아래와 같이 object.prototype
  33. // 맴버인 hasOwnProperty() 사용하여 필터링한다.
  34.  
  35. for (var n in Forin){
  36.     if (Forin.hasOwnProperty(n)){
  37.         alert(n); // test1, test2, test3
  38.     }
  39. }
  40.  
  41. // call 메소드를 활용하여 Object생성자를 통해 직접 메소드를 호출하게
  42. // 만들어 프로토타입 검색 비용을 즐여주었다.
  43. for (var n in Forin){
  44.     if (Object.hasOwnProperty.call(Forin, n)){
  45.         alert(n); // test1, test2, test3
  46.     }
  47. }
  48.  
  49.  
  50. // call 메소드를 활용하여 Object생성자를 통해 직접 메소드를 호출하게 만들어
  51. // 프로토타입 검색 비용을 줄여주고 또 메소드를 지역변수에 캐쉬하여
  52. // 한번 더 비용을 절감하는 효과를 주었다.
  53.  
  54. var hasOwnPropety = Object.hasOwnProperty;
  55. for (var n in Forin){
  56.     if (hasOwnPropety.call(Forin, n)){
  57.         alert(n); // test1, test2, test3
  58.     }
  59. }

위와 같은 결과가 나오는 이유는 내장 생성자인 Object 객체의 프로토타입 맴버를 사용자가 임의로 확장하면, 생성된 모든 객체에 실시간으로 Object 객체의 프로토타입 맴버가 반영되게 됩니다.

즉, for-in 문을 활용한 객체 순회 시 자신의 맴버가 아니라 할지라도 해당 결과처럼 목록을 노출하는 것입니다.

이 같은 혼돈을 방지하기 위해서라도 내장 생성자는 임의로 확장하지 않는 것을 권장하며, 어쩔 수 없는 경우로 확장할 때에는 순회 시 object.prototype 맴버인 hasOwnProperty() 메서드를 사용하여 자신의 맴버를 필터링 하는 작업을 따로 수행해야 합니다. 




7. 문자열

코드 작성 시 문자열을 다루는 일은 모든 프로그램상에서 가장 많이 일어나는 반복적인
행위이며, 그만큼 성능 최적화에 대해서도 고려를 가장 많이 해야 하는 부분이기도 합니다.


이전 브라우저들은 문자열 연산에 대해 지금과 같이 최적화를 하지 않았습니다.

문자열들을 연결한 결과를 만들기 위해서는 문자열과 문자열 사이에 각각의 문자열을 더한
중간 문자열들을 만들어 메모리에 적재하였고, 또 이렇게 만들어진 중간문자열을
매번 제거함으로써 결국 성능상 좋지 않은 결과를 가져왔습니다.



  1.     var x = 'x' + 'y';
  2.     var m = x; // 중간 문자열
  3.     x = m;
  4.      
  5.     alert(x); //xy
  6.      
  7.      
  8.     // 또 다른 문자열 연결 방법으로는 아래와 같이 배열을 활용하는 방법이 있습니다.
  9.     // 배열의 push() 메소드와 join() 메소드를 활용하여 문자열을 연결하는 방법 입니다.
  10.      
  11.      
  12.     var aStr = [];
  13.     aStr.push('str');
  14.     aStr.push('str');
  15.     alert(aStr.join('')); //strstr
  16.  
  17.  


이 방법은 이전의 + 연산자를 사용한 문자열 연결보다 성능상 효율적입니다.

이유는 이전 + 연산처럼 비효율적으로 중간 문자열을 담는 메모리를 만들고 지우는
동작을 하지 않기 때문입니다.


단, 최신 브라우저에서는 브라우저의 자바스크립트 엔진 최적화로 배열 기법을 사용하는
것보다 + 연산자를 사용하는 것이 성능상 더 효율적입니다.


다시 말해, IE8 이전 버전은 배열기법이 좋으며, IE8 이후 버전에는 + 연산자를 활용한
방법이 성능상 더 좋다는 것입니다.


즉, 현재 서비스 하는 지역과 사용자의 IE 버전 분포도에 따라 문자열 연결에 방법을
선택하는 것이 중요합니다.




8. 오랜 시간 지연되는 스크립트

자바스크립트는 단일 스레드를 기반으로 하는 언어입니다.

하나의 (창(window)), 혹은 하나의 (탭(tab))에서 오로지 한 번에 한 가지 동작씩만
수행할 수 있다는 말입니다.


이 말은 곧 하나의 동작이 오랜 시간 동안 끝나지 않는다면 사이트의 모든 흐름을 멈추게
할 수도 있다는 말과 같습니다.


모든 브라우저는 이와 같은 상황에 아래와 같은 반응을 보여줍니다.


1. IE는 스크립트에 의해 "실행된 문장의 수"를 기반으로 일정량 (기본 최댓값 5백만)보다 더 많이
실행 되었으면 대화상자를 보여준다.

2. FF는 스크립트가 "실행되고 있는 시간"을 기반으로 하여 지정된 시간보다 오랫동안 실행될 때 대화상자를 보여준다.


3. 사파리는 스크립트가 "실행되고 있는 시간"을 기반으로 하여 지정된 시간보다 오랫동안 실행될 때 대화상자를 보여준다.


4. 크롬은 아무런 제약을 두지 않고 가진 메모리를 다 써버리면 프로세스가 죽는다.


5. 오페라는 이에 대하여 어떠한 작업도 하지 않는다.



보통 이와 같은 상황이 나타나는 이유로는 과도한 DOM 작업, 과도한 반복문, 과도한
재귀호출에 대한 문제가 보통입니다.