성능을 높이는 코드 스타일

객체의 생성, 초기화 성능

많이 사용하지만 성능을 생각하지 않고 작성하는 코드에 객체 선언과 초기화 구문이 있다. 배열(Array) 형식의 객체와 오브젝트(Object) 형식의 객체를 생성하고 초기화하는 방법의 성능을 측정해 보고 두 코드 사이에 어떤 차이점이 있는지 비교해 보고자 한다.

배열의 생성, 초기화 성능 비교

배열을 생성자 혹인 리터럴([]) 형식을 사용해 객체를 생성할 수 있다.

1
2
3
4
5
// Array() 생성자 사용
var arr = new Array();

// 리터럴 형식으로 배열 생성
var arr = [];

성능 비교 방법은 http://jindo.dev.naver.com/jsMatch/index.html 사이트를 이용한다. (Chrome 79 버전 기준)

두 방법의 성능을 비교한 결과는 생성자를 사용했을때는 0.111s, 리터럴 형식으로 사용했을 경우 0.100s 의 시간이 걸렸다.

배열을 사용하려면 배열의 각 요소에 데이터를 할당해 초기화해야한다.
배열의 각 요소에 데이터를 할당하는 방법에도 여러가지가 있다. 그 가운데 가장 많이 볼 수 있는 방법인 접근자 []를 사용하는 방법과 push() 메서드를 사용하는 방법의 성능을 비교한다.

1
2
3
4
5
6
7
8
9
10
11
// 접근자 사용
var arr = [];
for (var i = 0; i < 1000; i++) {
arr[i] = i;
}

// push() 메서드 사용
var arr = [];
for (var i = 0; i < 1000; i++) {
arr.push(i);
}

결과: 접근자 사용 0.100s / push 사용 0.123s

push 메서드를 사용하는것 보다 접근자를 사용하는 것이 빠르다는 것을 확인할 수 있다. 배열의 생성과 초기화 방법을 비교한 결과, 배열을 사용할 때는 리터럴 형식으로 객체를 생성하고 Array.push() 메서드보다 접근자 []를 사용해 데이터를 추가하는 코드를 작성하는 것이 좀 더 최적화된 배열 사용법이라는 사실을 확인할 수 있다.

오브젝트(Object) 객체의 생성, 초기화 성능 비교

오브젝트(Object) 객체도 배열처럼 객체를 생성하고 초기화하는 다양한 방법이 있다. 가장 많이 사용하는 방법인 리터럴({})을 사용하는 방법과 생성자를 사용하는 방법의 성능을 테스트해보고 어떤 방법으로 객체를 생성하고 초기화하는 것이 효과적인지 살펴보고자 한다.

1
2
3
4
5
// 리터럴 사용
var obj = {};

// 생성자 사용
var obj = new Object();

결과: 리터럴 사용 0.104s / 생성자 사용 0.100s

배열과 다르게 생성자를 사용하는 것이 시간이 적게 걸리지만 차이가 적으므로 어떤 방법이 성능이 월등히 좋다고 판가름하기 어렵다. 리터럴 형식이 코드 크기를 좀 더 줄일 수 있는 방법이기 때문에 코드를 다운로드 하는 시간 관점에서 성능에 더 좋다고 볼 수는 있다.

객체 초기화 방법에는 연산자를 이용한 데이터 삽입과 []를 이용한 데이터 삽입이 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 연산자 이용
var obj = {};
obj.a = 1;
obj.b = 2;
obj.c = 3;
obj.d = 4;
obj.e = 5;
obj.f = 6;
obj.g = 7;
obj.h = 8;
obj.i = 9;
obj.j = 10;

// [] 이용
var obj = {};
obj["a"] = 1;
obj["b"] = 2;
obj["c"] = 3;
obj["d"] = 4;
obj["e"] = 5;
obj["f"] = 6;
obj["g"] = 7;
obj["h"] = 8;
obj["i"] = 9;
obj["j"] = 10;

결과: 연산자 이용 0.102s / [] 이용 0.100s

Object 객체에 데이터를 삽입하는 초기화 방법에 대한 성능 테스트 결과도 Object 객체 생성에 대한 테스트 결과와 유사헀다. 객체의 초기화도 생성과 마찬가지로 한 가지 방식이 더 성능이 좋다고 판단할 수 없으며, 작성하는 코드의 크기와 주요 대상 브라우저 및 코드의 가독성과 유지 보수를 감안해 적절한 방식을 택하면 되겠다.

스코프 체인 탐색과 성능

자바스크립트 성능을 다루는 책에서 항상 빠지지 않는 부분이 스코프 체인이다. 개발자가 작성한 코드 자체의 성능이 런타임 성능에도 많은 영향을 준다. 런타임 환경에서 가장 많이 발생하는 브라우저의 작업 가운데 자바스크립트의 실행 성능을 저해하는 요인이 변수, 객체, 함수 등의 메모리상의 위치를 찾는 탐색 작업이다.

스코프 체인이란?

자바스크립트의 함수를 실행하면서 어떤 속성(변수, 객체 등)에 접근해야 할 때 해당 속성을 효율적으로 탐색하도록 속성을 일정한 객체 단위로 분류하고 각 객체에 접근하기 위한 객체의 참조를 특정한 공간에 저장해 둔다. 이 공간이 바로 스코프 체인이다.

스코프 체인의 구성 요소에는 활성화 객체와 전역 객체가 있다. 함수에서 접근할 수 있는 모든 속성 가운데 함수 내부에서만 접근할 수 있는 함수의 지역변수나 this, arguments 객체 등의 속성은 스코프 체인의 활성화 객체에 포함돼 관리 된다. 함수 외부에서도 접근할 수 있는 window, document, 전역함수, 전역변수와 같은 속성은 스코프 체인의 전역 객체에 포함돼 관리 된다.
전역 객체는 자바스크립트 동작시 어디서나 항상 접근 가능한 데이터를 포함하고 있기 때문에 동작하는 모든 시간 동안 존재하지만, 활성화 객체는 실행 중인 함수 내부 데이터를 포함하기 때문에 함수가 실행되는 동안에만 존재한다.

실행 문맥(Execution Context)은 함수가 동작하는 환경을 나타내며, 브라우저 내부에서 사용되는 객체다. 실행 문맥은 함수가 실행될 때 새로 생성되고 함수가 종료될 때 소멸되며 함수의 스코프 체인에 대한 참조를 가지고 있게 된다. 실행 문맥은 자신과 연관된 함수의 스코프 체인을 참조하고 있으며, 함수에서 접근해야할 어떤 속성의 탐색 경로는 실행 문맥 > 스코프 체인 > 활성화 객체 > 스코프 체인 > 전역 객체 와 같이 구성된다.

지역변수를 활용한 스코프 체인 탐색 성능 개선

스코프 체인의 탐색 방법을 살펴보면 여러 개의 활성화 객체와 전역 객체를 탐색하면서 접근하려는 속성이 있는지 확인하는 과정이 반복된다. 그렇다면 첫 번째로 탐색하는 활성화 객체에 찾고자 하는 속성이 있는 경우 추가로 발생할 수 있는 다른 활성화 객체, 전역 객체를 탐색하는 과정을 줄여 성능을 향상시킬 수 있을 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 함수 내에서 전역 스코프 변수에 직접 접근하는 방법
window.htmlstring = [];
function makeList () {
htmlstring.push("<ul>");
for (var i =0; i < 100; i++) {
htmlstring.push(`<li>value: ${i}</li>`);
}
htmlstring.push("</ul>");
}

makeList();

// 지역변수로 참조해 전역 스코프 변수에 접근하는 방법
window.htmlstring = [];
function makeList () {
var htmlstr = htmlstring;
htmlstr.push("<ul>");
for (var i =0; i < 100; i++) {
htmlstr.push(`<li>value: ${i}</li>`);
}
htmlstr.push("</ul>");
}

makeList();

결과: 전역 스코프 변수 접근 0.119s / 지역변수 참조 0.100s

전역 객체에 존재하는 htmlstring 속성을 makeList() 함수의 지역변수에 저장해 활성화 객체에서 바로 찾을 수 있게 한것이다. 최초 한 번은 활성화 객체와 전역 객체를 모두 탐색해야한다. 하지만 그 이후에는 활성화 객체에 저장된 htmlstr 속성으로 전역변수인 htmlstring 객체에 접근할 수 있으니 활성화 객체를 거쳐 전역 객체까지 탐색할 필요가 없어진다.

반복문과 성능

자바스크립트의 반복문인 for, for-in, while, do-while 구문에도 성능 차이가 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 성능 테스트 전에 배열 초기화
arr = [];
for (var i=0; i < 400; i++) {
arr[i] = i;
}

// for
for (var i = 0, len = arr.length; i < len; i++) {
arr[i]++;
}

// for-in
for (var i in arr) {
arr[i]++;
}

// while
var i = 0, len = arr.length;
while (i < len) {
arr[i]++;
i++;
}

// do-while
var i = 0, len = arr.length;
do {
arr[i] = i;
i++;
} while (i < len);

결과: for 0.113s / for-in 0.505s / while 0.108s / do-while 0.100s

for-in의 성능이 압도적으로 떨어지는 것을 확인할 수 있다. for-in 이외의 반복문은 주어진 배열 객체를 배열의 특성에 맞게 순차적으로 모든 요소를 탐색한다. 반면 for-in 구문은 인자로 주어진 배열을 배열이 아닌 일반 객체로 취급하며, 반복 시점마자 객체의 모든 속성을 무작위로 탐색한다. 이러한 탐색 방법의 차이로 다른 반복문에 비해 배열 탐색에서 현저하게 느리다.

조건문과 성능

반복문만큼이나 조건문 또한 자바스크립트를 포함한 프로그래밍 전반에서 필수적으로 사용하는 요소다. 자바스크립트에는 if, if-else, switch, 삼항연산자 등의 조건문이 있다.

조건문의 성능 비교

먼저 true와 false만 판단하는 최소한의 조건 분기를 처리하는 코드로 if, switch, 삼항연산자의 성능을 측정했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// if-else
function toEnglish(value) {
var number = "zero";
if (value === 1) {
number = "one";
} else if (value === 2) {
number = "two";
} else if (value === 3) {
number = "three";
} else if (value === 4) {
number = "four";
} else if (value === 5) {
number = "five";
} else if (value === 6) {
number = "six";
} else if (value === 7) {
number = "seven"
} else if (value === 8) {
number = "eight";
} else if (value === 9) {
number = "nine";
} else if (value === 10) {
number = "ten";
} else {
number = "null";
}

return number;
}

for (var i=0; i < 12; i++) {
toEnglish(i);
}

// switch-case
function toEnglish(value) {
var number = "zero";
switch(value) {
case 1:
number = "one"
break;
case 2:
number = "two";
break;
case 3:
number = "three";
break;
case 4:
number = "four";
break;
case 5:
number = "five";
break;
case 6:
number = "six";
break;
case 7:
number = "seven"
break;
case 8:
number = "eight";
break;
case 9:
number = "nine";
break;
case 10:
number = "ten";
break;
default:
number = "null";
break;
}
return number;
}

for (var i=0; i < 12; i++) {
toEnglish(i);
}

// 삼항연산자
function toEnglish(value) {
var number = false;
number = (value === 1) ?
"one" : (value === 2) ?
"two" : (value === 3) ?
"three" : (value === 4) ?
"four" : (value === 5) ?
"five" : (value === 6) ?
"six" : (value === 7) ?
"seven" : (value === 8) ?
"eight" : (value === 9) ?
"nine" : (value === 10) ?
"ten" : "null";

return number;
}

for (var i=0; i < 12; i++) {
toEnglish(i);
}

결과: if-else 0.111s / switch 0.111s / 삼항연산자 0.100s

조건의 개수가 많지 않으므로 조건문 사이에 별다른 성능 차이가 없다. 단지, 일반적으로 조건 판단 요소가 많아질수록 switch-case 구문의 성능이 좀 더 좋다는 점은 알아두는 것이 좋다.

문자열 연산과 성능

문자열 생성 비교

1
2
3
4
5
// String 객체 이용
var str = new String("test");

// 리터럴 이용
var str = "test";

결과: String 객체 0.103s / 리터럴 이용 0.100s

측정한 시간이 지극히 짧지만 String 객체보다는 리터럴을 사용하는 것이 좋다.

문자열 연산 성능 비교

여러가지 데이터를 조합해 긴 문자열을 생성해야 할 때가 많다. 이때 가장 많이 사용하는 방법이 +, += 연산자를 이용하거나 Array.join() 메서드를 이용하는 방법이다.

1
2
3
4
5
6
7
8
9
10
11
12
// += 연신자
str = "";
for (var i = 0; i < 100; i++) {
str += "test";
}

// Array.join
arr = [];
for (var i = 0; i < 100; i++) {
arr[i] = "test";
}
arr.join("");

결과: += 연산자 0.398s / Array.join 0.100s

Array.join() 메서드를 이용하는것이 월등한 성능을 낸다는 것을 확인할 수 있다. += 연산자는 두 문자열을 합친 새로운 문자열을 만들고 새로운 메모리 위치에 저장함과 동시에 기존 문자열에 대한 참조를 변경하는 연산을 반복적으로 실행해야 한다. 하지만 Array.join() 메서드로 연산하면 비교적 메모리에 효율적으로 접근할 수 있는 배열을 사용한다.

결론

  • 배열은 리터럴([])방식으로 생성하고 push() 메서드가 아닌 접근자([])로 데이터를 할당하는 코드의 성능이 더 좋다.
  • 객체는 어떤 방식이든지 비슷한 성능을 낸다. 가독성과 유지보수의 편리성을 고려해 적절한 방식으로 사용하면 된다.
  • 반복문 가운데 for-in의 성능이 월등히 낮다.
  • 조건문의 종류에 따른 성능 차이는 거의 없지만 조건이 많아지면 switch-case 성능이 좀 더 좋다.
  • 문자열을 연산할 때는 += 연산자 보다는 Array.join()을 이용하는 방법이 더 좋다.

참조: 자바스크립트 성능 이야기

댓글

You need to set client_id and slot_id to show this AD unit. Please set it in _config.yml.