[js] Array.prototype 사용하기

현재 대부분의 브라우저는 배열에 slice 함수를 내장하고 있습니다.
물론 배열에 대해서는 잘 작동합니다.
문제는 이 함수를 다른 곳에 활용할 때 호환성이 제 각각이라는 점입니다. slice를 예로 들어 배열 내장 함수를 매우 깊이 탐구해 보겠습니다.

 
 
 

컴파일 과정이 없는 자바스크립트top

  1. 자바스크립트는 컴파일 과정이 없으므로 100% 실행 시점에 해석되고 실행됩니다.
  2. 따라서 개발자가 작성한 코드는 별도의 최적화 과정을 거칠 수 없습니다.
  3. 바로 이 점이 최대한 native함수를 이용해야 하는 이유입니다.

같은 수준의 알고리즘 최적화를 구현했다고 해도, 미리 c로 컴파일 된 내장 함수가 훨씬 고속으로 작동합니다. 내장 함수를 최대한 활용하는 것이야말로 자바스크립트를 고속화 시키는 첫 번째 요령입니다.

slice를 사용하는 경우
잠시 배열을 잊고 slice라는 함수만 집중해보죠. 이 함수를 매우 기저까지 내려가서 개념을 잡아보면 다음과 같습니다.

  1. 어떤 데이터 구조체가 있다.
  2. 구조체는 속성이 0,1,2..와 같은 숫자로 매겨져 있고 해당 속성에 원하는 값이 들어있다.
  3. length라는 속성이 존재한다.
  4. 이상에서 설명한 구조체에서 번호 속성을 이용하여 일부를 잘라내 새로운 배열을 만들어낸다.

그럼 여기서 왜 배열을 설명하기 위해 1, 2, 3에 걸쳐서 묘사를 했냐는 겁니다.
그 이유는 배열이 아니라도 1, 2, 3을 만족하는 구조체를 만들어 낼 수 있기 때문입니다.
예를 들어 아래와 같은 오브젝트를 생각해보죠.

var temp = {0:'one', 1:'two', 2:'three', length:3};

temp 는 분명 오브젝트지만 위에 언급한 1, 2, 3 조건을 전부 만족하고 있습니다.
따라서 slice의 알고리즘을 고려하건데 충분히 작동할 가능성이 있어 보입니다.

 
 
 

slice의 내부top

더 쉽게 이해하기 위해 slice 함수를 직접 수동으로 작성해보죠.

function slice( $start, $end ){
	var i, j, result;
	result = [];
	j = $end === undefined ? this.length : $end;
	for( i = $start ; i < j ; i++ )
		result[result.length] = this[i];
	return result;
}

위 로직에서 중요한 점은 결국 대괄호 구문으로 해당 키를 얻어낸다는 점입니다.오브젝트도 모든 속성을 대괄호 구문으로 얻을 수 있기 때문에 동일하게 작동할 수 있습니다. 실제로 적용해봅니다.

var slice = Array.prototype.slice;
var temp = {0:'one', 1:'two', 2:'three', length:3};
alert( slice.call( temp, 1 ) ); // two, three 출력

 
 
 

Array.prototype.slicetop

이제 slice에 대해 충분히 이해했으니 같은 기능을 가진 native 함수를 불러봅시다.

temp = {0:'one', 1:'two', 2:'three', length:3};
alert( Array.prototype.slice.call( temp, 1 ) ); // two, three 출력

잘 작동합니다. 속도 면에선 당연히 자바스크립트로 작성한 slice함수보다 c로 컴파일 된 배열의 내장 함수가 훨씬 고속입니다.
길게 쓰는 게 귀찮다면 미리 변수에 담아둘 수 있습니다.

var slice = Array.prototype.slice;
var temp = {0:'one', 1:'two', 2:'three', length:3};
alert( slice.call( temp, 1 ) ); // two, three 출력

이 기능은 현재 IE6 부터 모바일 브라우저까지 거의 모든 브라우저에서 공통적으로 처리됩니다. 즉 배열 외에 오브젝트를 배열처럼 흉내 내도 잘 작동한다는 뜻입니다.

 
 
 

배열 흉내낸 걸 왜 쓸까?top

그거야 당연히 가볍기 때문입니다. 아래와 같이 실험해보세요.

var i, start, temp, result1, result2;

start = new Date()*1;
i = 9999999;
while( i-- ) temp = [];
result1 = new Date()*1 - start;

start = new Date()*1;
i = 9999999;
while( i-- ) temp = {};
result2 = new Date()*1 - start;

alert( result1 +":"+ result2 );

얼마나 차이가 나냐는 컴퓨터와 브라우저환경에 따라 다르지만 확실한 공통점은 오브젝트가 훨씬 가볍다는 점입니다.
0 ~ 99 을 배열에 대입하는 경우와 배열을 흉내 낸 오브젝트에 대입하는 코드는 의외로 거의 비슷합니다.

var result, i;

//배열에 대입하기
result = [];
i = 0;
while( i < 100 ) result[result.length] = i++;

//오브젝트에 대입하기
result = {};
i = 0;
while( i < 100 ) result[result.length++] = i++;

배열은 삽입하면 길이가 자동으로 늘어나지만 오브젝트는 직접 길이도 관리해야 합니다.
이러한 배열과 비슷한 오브젝트를 이제부터 리스트라고 부르겠습니다.
* 사실 이 이름은 제가 임의로 부여한 것은 아니고 자바스크립트에서 흔히 사용되는 구조입니다.

 
 
 

가장 흔한 리스트top

자바스크립트에서 가장 흔하게 접하는 리스트는 바로 arguments객체입니다. 예를 들어 다음과 같이 함수를 호출해보죠.

function test(){
	alert( arguments[0] + ',' + arguments[1] );
}
test( 'a', 'b' ); // a,b 출력

arguments는 자바스크립트에서 일단 함수가 호출되면, 전달한 인자를 이용해 생성하는 리스트입니다.
따라서 배열과는 다르지만 숫자로 된 키에 인자들이 순서대로 들어있고 무엇보다 length라는 속성이 존재합니다.
앞 서 다루고 있던 리스트 형태와 완전히 일치합니다. 따라서 아래와 같이 전달된 인자 중에 일부만 추출하기 위해 slice를 사용할 수 있습니다.

function test(){
	alert( Array.prototype.slice.call( arguments, 1 ) );
}
test( 0, 1, 2, 3 ); // 1,2,3 출력

인자로 전달된 것은 0, 1, 2, 3이었으나 slice를 통해 앞에 하나를 제거했으므로 결과가 1, 2, 3이 됩니다.
이 기능 역시 모든 브라우저에 공통적으로 사용할 수 있습니다.

 
 
 

HTMLElementList에 적용하기top

준비 운동을 마쳤으니 이제 슬슬 진짜 문제로 들어가 보죠. DOM api는 다양한 경우에 HTMLElementList라고 부르는 형식을 반환하게 되어있습니다.
예를 들어 document.getElementsByTagName( ‘div’ ) 와 같은 함수가 이에 해당됩니다.
사실 getElementById를 제외하면 대부분은 리스트를 반환한다고 생각해야 합니다.
이러한 리스트 중 일부만 얻고 싶다면 arguments와 마찬가지로 slice를 사용할 수 있습니다.

var result = document.getElementsByTagName( 'div' );
result = Array.prototype.slice.call( result, 2, 5 );

위와 같이 하면 얻어온 div리스트에서 인덱스 2, 3, 4 번에 해당되는 녀석만 정리하여 배열로 반환 받게 됩니다.
하지만 부분 집합을 얻지 않더라도 사용방법이 다양합니다.
실은 대부분의 DOM api가 반환한 리스트는 특수한 동적 리스트인데 일단 반환이 된 후에도 DOM이 수정되면 지속적으로 그 결과를 업데이트합니다.
즉 아래와 같은 일이 일어납니다.

var result = document.getElementsByTagName( 'div' );
alert( result.length ); // 일단 10개가 있다고 하자.

//자식을 추가하자.
document.body.appendChild( document.createElement( 'div' ) );
alert( result.length ); // 11이 되어있다!

반환된 리스트도 관리대상자라는 점입니다. 따라서 DOM과 result 사이의 관계를 끊어버려야 리스트를 확정 지을 수도 있고 전체적으로 DOM이 관리할 대상이 줄어 더 빨라집니다.
따라서 HTMLElementList를 재활용하거나 지속적으로 사용하는 경우 배열이나 오브젝트로 새롭게 만들어야 성능 상 유리합니다.
예를 들어 배열에 담는다면 아래와 같이 될 것입니다.

var temp = document.getElementsByTagName( 'div' );
var i = temp.length;
var result = [];
while( i-- ) result[i] = temp[i];

하지만 native함수인 slice를 이용하면 보다 간단하고 빠르게 처리할 수 있습니다.

var temp = document.getElementsByTagName( 'div' );
var result = Array.prototype.slice.call( temp, 0 );

이상에서 살펴본 바처럼 Array,prototype.xxx를 이용하면 HTMLElementList에 대해 매우 간단하게 사본이나 부분집합을 얻을 수 있습니다. 그런데…

 
 
 

리스트와 Array.prototype의 호환성top

일단 Array에서 제공하는 함수는 자신을 변화시키는 함수와 별도의 결과를 출력하는 함수로 나눌 수 있습니다. 자신을 변화시키는 함수의 경우 본인의 길이도 변하고 요소들의 인덱스도 변화하는 등 배열 고유의 구조에 대한 접근이 필요합니다. 따라서 이러한 함수군은 리스트가 빌려 쓸 수 없습니다.

그러한 함수는 아래와 같습니다.

  • Array.prototype.unshift;
  • Array.prototype.shift;
  • Array.prototype.pop;
  • Array.prototype.push;
  • Array.prototype.splice;
  • Array.prototype.sort;

위의 함수는 배열 자체를 변화시키기 때문에 진짜 배열이 아니면 작동하지 않습니다. 하지만 아래와 같은 함수들은 새로운 객체를 결과로 반환하기 때문에 가능합니다.

  • Array.prototype.concat;
  • Array.prototype.slice;
  • Array.prototype.join;
  • Array.prototype.indexOf;

하지만 indexOf의 경우 많은 브라우저에서 미 구현 되어있습니다. 왜냐면 현재 공식 버전의 자바스크립트 API에는 포함되지 않기 때문에 별도의 최신 버전을 지원하는 브라우저에만 존재하기 때문 입니다.

concat, slice join, indexOf는 기본적으로 리스트에 적용할 수 있고 실제로 작동합니다.

문제는 오브젝트로 제작한 리스트에 정상 작동하더라도 HTMLElementList에는 정상적으로 작동하지 않는다는 점입니다.

 
 
 

IE 6, 7, 8과 HTMLElementList의 문제top

9.0 이전의 IE는 DOM관련된 객체나 반환결과가 자바스크립트 객체가 아니라 전용 activeX객체입니다. 이 경우 아예 자바스크립트 코어 측의 배열 함수들이 접근 자체가 불가능합니다.
따라서 오브젝트의 리스트는 멀쩡히 처리할지라도 HTMLElementList는 처리할 수 없습니다. 이 경우 native함수를 사용하는 걸은 포기해야 합니다.

하지만 이렇게 되면 통합된 함수를 만들 수 없고 단편화 됩니다. 특히 이러한 처리를 하기 위해 브라우저와 버전을 인식하는 브라우저 감지는 매우 좋지 않습니다. IE8의 경우 공식적인 XP의 마지막 브라우저인데 MS사가 IE8한정으로 HTMLElementList 처리를 자바스크립트 native화 해버릴 수도 있는 일입니다. 따라서 브라우저 감지가 아니라 객체 탐지를 통해 해결해야 합니다. 해당 배열 함수가 HTMLElementList 처리가능한 지 확인할 방법은 속성이나 함수로는 제공되지 않습니다. ajax객체 감지처럼 try…catch를 사용할 수 밖에 없을 듯하군요. 그럼 간단히 slice를 패치해 보죠.

var slice = Array.prototype.slice;

try{

	slice.call( document.getElementsByName('*'), 1 );

}catch( e ){

	slice = function( $start, $end ){
		var i, j, result;
		result = [];
		j = arguments.length == 1 ? this.length : $end;
		for( i = $start ; i < j ; i++ )
			result[result.length] = this[i];
		return result;
	};

}

머 별거 있나요. 직접 slice해보고 이상 있으면 자바스크립트 함수로 대체할 뿐입니다.

 
 
 

결론top

  1. 배열 내장 함수는 배열 외에도 오브젝트 리스트에 쓸모가 많습니다. 이러한 오브젝트를 이용한 리스트의 예로는 argumentsHTMLElementList가 있습니다.
  2. 하지만 IE의 문제 등 배열 내장 함수를 항상 리스트에 적용할 수 있는 것은 아닙니다.
  3. 이러한 모든 고난에도 불구하고 되도록이면 native함수를 사용하도록 노력해야 합니다. 결국 아무리 정교하게 자바스크립트를 작성해도 c로 컴파일 된 native함수를 이길 수는 없기 때문입니다.