Skip to content

Kyunghwa Yoo

속 깊은 자바스크립트

javascript4 min read

스코프와 클로저

  • 스코프를 생성하는 구문
    • function
    • with
      • with 블록 안에서 매개변수 객체의 속성들이 지역변수처럼 사용할 수 있는 것을 가리킴
    • catch
      • catch 블록에서 사용되는 매개변수 객체 (Error 객체)는 블록 안에서만 사용할 수 있다.

with 구문을 자제해야 하는 이유

  • 변수 탐색 비용이 더 발생할 수 있다. 상위 블록 변수를 추가로 등록할 때 with 블록을 탐색한 후 상위 블록을 탐색해서 변수가 없는 것을 확인한다.
  • 소스코드가 모호해 질 수 있다. with 블록 안의 변수가 with 매개변수 객체의 변수인지 상위 변수인지 알기 어렵다.
  • ECMA6 표준에서 제외된다.

클로저 이해하기

첫 번째 예제

1var countFactory = (function() {
2 var staticCount = 0;
3
4 return function() {
5 var localCount = 0;
6
7 return {
8 increase: function() {
9 return {
10 static: ++staticCount,
11 local: ++localCount
12 };
13 },
14 decrease: function() {
15 return {
16 static: --staticCount,
17 local: --localCount
18 };
19 }
20 };
21 };
22})();
23
24var counter = countFactory();
25var counter2 = countFactory();
26
27console.log(counter.increase());
28console.log(counter.increase());
29console.log(counter2.decrease());
30console.log(counter.increase());

두 번째 예제

1<!DOCTYPE html>
2<html>
3<head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width">
6 <title>클로저 예제</title>
7</head>
8<body>
9 <button id="btnToggle">Toggle Pending</button>
10 <div id="divPending">Pending</div>
11 <script>
12 (function() {
13 var pendingInterval = false;
14 var div = document.getElementById("divPending");
15 var btn = document.getElementById("btnToggle");
16
17 function startPending() {
18 if (div.innerHTML.length > 13) {
19 div.innerHTML = "Pending";
20 }
21 div.innerHTML += ".";
22 };
23
24 btn.addEventListener("click", function() {
25 if (!pendingInterval) {
26 pendingInterval = setInterval(startPending, 500);
27 } else {
28 clearInterval(pendingInterval);
29 pendingInterval = false;
30 }
31 });
32 })();
33 </script>
34</body>
35</html>

세 번째 예제

1<!DOCTYPE html>
2<html>
3<head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width">
6 <title>클로저 예제</title>
7</head>
8<body>
9 <div id="wrapper">
10 <button data-cb="1">Add div</button>
11 <button data-cb="2">Add img</button>
12 <button data-cb="delete">Clear</button>
13 Adding below... <br/>
14 <div id="appendDiv"></div>
15 </div>
16 <script>
17 (function() {
18 var appendDiv = document.getElementById("appendDiv");
19 document.getElementById("wrapper").addEventListener("click", append);
20
21 function append(e) {
22 var target = e.target || e.srcElement || event.srcElement;
23 var callbackFunction = callback[target.getAttribute("data-cb")];
24 appendDiv.appendChild(callbackFunction());
25 };
26
27 var callback = {
28 "1": (function () {
29 var div = document.createElement("div");
30 div.innerHTML = "Adding new div";
31 return function () {
32 return div.cloneNode(true);
33 };
34 })(),
35 "2": (function () {
36 var img = document.createElement("img");
37 img.src = "https://dummyimage.com/600x400/000/fff";
38 return function () {
39 return img.cloneNode(true);
40 };
41 })(),
42 "delete": function() {
43 appendDiv.innerHTML = "";
44 return document.createTextNode("Cleared");
45 }
46 };
47 })();
48 </script>
49</body>
50</html>
  • 중복해서 DOM을 탐색하거나 특정 템플릿을 가지는 DOM을 생성할 때는 클로저로 초기화하여 사용하는 것이 성능에 유리하다.
  • 클로저는 이럴 때 사용하자
    • 반복적으로 같은 작업을 할 때
    • 같은 초기화 작업이 지속적으로 필요할 때
    • 콜백 함수에 동적인 데이터를 넘겨주고 싶을 때

클로저의 단점

  • 메모리 소모
    • 루프를 돌면서 클로저를 계속 생성하는 설계는 지양해야 한다.
  • 스코프 생성과 이후 변수 조회에 따른 성능저하
    • 가장 하위에 있는 함수에서 상위에 있는 변수에 접근하고자 할 때 탐색 비용이 발생한다.

변수

  • 새로운 스코프를 시작할 때 var 를 한번만 쓰고 그 스코프에서 사용할 변수들을 나열하는 것이 최적화 및 성능 측면에서 효율적이다.
  • 글로벌 변수의 사용을 최소화하기 위하여 클로저나 모듈 패턴을 사용하는 것이 좋다.
  • 상위 스코프의 변수를 계속 참조하는 것보다 한번만 지역변수에 상위 스코프 변수를 할당하고 그 지역변수를 사용하는 것이 더 효율적이다.

프로토타입, 객체지향, 상속

  • Function을 생성할 때 기본적으로 프로토타입 속성이 생성된다.
  • 이 프로토타입을 다른 객체로 설정함으로써 다른 객체의 속성들과 함수들을 공유 또는 상속할 수 있다.
  • 객체는 프로토타입과 내부 링크로 연결되어 있어 프로토타입의 속성들을 자기의 속성인 것 처럼 접근할 수 있다.
  • 객체에서 직접 this.constructor.prototype 으로 접근하지 않으면 프로토타입의 값은 수정되지 않고 현재 객체 내에 같은 이름의 속성이 설정되어 프로토타입의 설정값이 가려진다.
  • new 로 객체를 생성할 때 프로토타입은 객체 간 공유되어 메모리 자원이 절약될 수 있다.
  • 여러 개의 프로토타입 체인을 만들 경우 속성 조회에 있어서 성능 저하가 있을 수도 있다.
  • new 는 생성자 기반 상속으로 사용되며, Object.create 는 객체 기반 상속으로 사용된다.
  • 객체지향을 위한 classextends 키워드가 정의되었지만 호환성 검토가 필요하다.
  • new 와 생성자로 객체를 생성하는 것이 Object.create 로 생성하는 것보다 성능상 유리하다.
  • 성능 이슈는 객체를 다량으로 사용하는 경우 조심하면 좋지만, 특별한 경우가 아니면 체감하기 힘들다.

디자인패턴

모듈 패턴

  • 모듈패턴은 글로벌 변수를 최소화하고 라이브러리나 API의 소스 관리에 유용하다.

이벤트 델리게이션 패턴

  • 이벤트 델리게이션 패턴은 여러 DOM에 이벤트 핸들러를 걸어야 하는 경우와 동적인 DOM의 이벤트 처리를 해야 하는 경우 유용하다.
  • 이벤트 처리 단계는 [캡처링]-[대상]-[버블링] 의 세 단계로 이루어져 있다.
1<!DOCTYPE html>
2<html>
3<head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width">
6 <title>이벤트 캡처링, 버블링</title>
7 <style>
8 div {
9 border: none;
10 }
11 .divOutside {
12 width: 200px;
13 height: 200px;
14 background-color: lightgreen;
15 }
16 .divMiddle {
17 width: 150px;
18 height: 150px;
19 background-color: lightblue;
20 }
21 .divInside {
22 width: 100px;
23 height: 100px;
24 background-color: pink;
25 position: relative;
26 }
27 .divFloat {
28 position: absolute;
29 left: 210px;
30 height: 50px;
31 width: 50px;
32 background-color: lightgray;
33 }
34 .highlight {
35 background-color: black;
36 }
37 </style>
38</head>
39<body>
40 <div id="divBubblingOutside" class="divOutside">
41 <div id="divBubblingMiddle" class="divMiddle">
42 <div id="divBubblingInside" class="divInside">
43 Bubbling
44 <div id="divBubblingFloat" class="divFloat"></div>
45 </div>
46 </div>
47 </div>
48 <br/>
49 <div id="divCapturingOutside" class="divOutside">
50 <div id="divCapturingMiddle" class="divMiddle">
51 <div id="divCapturingInside" class="divInside">
52 Capturing
53 <div id="divCapturingFloat" class="divFloat"></div>
54 </div>
55 </div>
56 </div>
57 <script>
58 (function() {
59 document.getElementById("divBubblingOutside")
60 .addEventListener("click", function() {
61 this.classList.toggle('highlight');
62 alert("Outside Bubbling");
63 this.classList.toggle("highlight");
64 });
65 document.getElementById("divBubblingMiddle")
66 .addEventListener("click", function() {
67 this.classList.toggle("highlight");
68 alert("Middle Bubbling");
69 this.classList.toggle("highlight");
70 });
71 document.getElementById("divBubblingInside")
72 .addEventListener("click", function() {
73 this.classList.toggle("highlight");
74 alert("Inside Bubbling");
75 this.classList.toggle("highlight");
76 });
77 document.getElementById("divBubblingFloat")
78 .addEventListener("click", function() {
79 this.classList.toggle("highlight");
80 alert("Float Bubbling");
81 this.classList.toggle("highlight");
82 });
83 document.getElementById("divCapturingOutside")
84 .addEventListener("click", function() {
85 this.classList.toggle('highlight');
86 alert("Outside capturing");
87 this.classList.toggle("highlight");
88 }, true);
89 document.getElementById("divCapturingMiddle")
90 .addEventListener("click", function() {
91 this.classList.toggle("highlight");
92 alert("Middle capturing");
93 this.classList.toggle("highlight");
94 }, true);
95 document.getElementById("divCapturingInside")
96 .addEventListener("click", function() {
97 this.classList.toggle("highlight");
98 alert("Inside capturing");
99 this.classList.toggle("highlight");
100 }, true);
101 document.getElementById("divCapturingFloat")
102 .addEventListener("click", function() {
103 this.classList.toggle("highlight");
104 alert("Float capturing");
105 this.classList.toggle("highlight");
106 }, true);
107 })();
108 </script>
109</body>
110</html>
  • event.stopPropagation(); 으로 이벤트 단계를 중단할 수 있다.

프락시 패턴

  • 프락시패턴을 통해 다른 객체에 접근하게 해주거나 다른 함수를 호출하게 해줄 수 있다.
  • 프락시패턴으로 wrapper 함수를 만들 수 있다. 다른 라이브러리나 모듈을 사용하기 전에 전처리 를 편하게 할 수 있다.
1(function () {
2 function wrap(func, wrapper){
3 return function() {
4 var args = [func].concat(Array.prototype.slice.call(arguments));
5 return wrapper.apply(this, args);
6 };
7 }
8
9 function existingFunction() {
10 console.log("Existing function is called with arguments");
11 console.log(arguments);
12 }
13
14 var wrappedFunction = wrap(existingFunction, function (func) {
15 console.log("Wrapper function is called with arguments");
16 console.log(arguments);
17 func.apply(this, Array.prototype.slice.call(arguments, 1));
18 });
19
20 console.log("1. Calling existing function");
21 existingFunction("existingFunction First argument", "existingFunction Second argument", "existingFunction Third argument");
22
23 console.log("\n2. Calling wrapped function");
24 wrappedFunction("wrappedFunction First argument", "wrappedFunction Second argument", "wrappedFunction Third argument");
25})();
  • 프락시패턴으로 로그를 남기는 기능을 구현할 수도 있다.
1(function() {
2 var car = {
3 beep: function beep() {
4 console.log('beep');
5 },
6 brake: function brake() {
7 console.log('stop');
8 },
9 accelerator: function accelerator() {
10 console.log('go');
11 }
12 };
13
14 function wrap(func, wrapper) {
15 return function() {
16 var args = [func].concat(Array.prototype.slice.call(arguments));
17 return wrapper.apply(this, args);
18 };
19 }
20
21 function wrapObject(obj, wrapper) {
22 var prop;
23 for(prop in obj) {
24 if (obj.hasOwnProperty(prop) && typeof obj[prop] === 'function') {
25 obj[prop] = wrap(obj[prop], wrapper);
26 }
27 }
28 }
29
30 wrapObject(car, function(func) {
31 console.log(func.name + ' has been invoked');
32
33 func.apply(this, Array.prototype.slice(arguments, 1));
34 });
35
36 car.accelerator();
37 car.beep();
38 car.brake();
39})();

데코레이터 패턴

  • 호출 대상이 되는 객체에 추가 기능을 자유롭게 추가하는 패턴이다.
  • 검증 도구 (validation check) 에 유용하다.
1<!DOCTYPE html>
2<html>
3<head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width">
6 <title>데코레이터 패턴</title>
7</head>
8<body>
9<form id="personalInformation">
10 <label>First name: <input type="text" class="validate" data-validate-rules="required alphabet" name="firstName"></label><br/>
11 <label>Last name: <input type="text" class="validate" data-validate-rules="required alphabet" name="lastName"></label><br/>
12 <label>Age: <input type="text" class="validate" data-validate-rules="number" name="age"></label><br/>
13 <label>Gender:
14 <select class="validate" data-validate-rules="required">
15 <option>Male</option>
16 <option>Female</option>
17 </select>
18 </label><br/>
19 <input type="submit">
20</form>
21<script>
22 (function() {
23 var formPersonalInformation = document.getElementById("personalInformation"),
24 validator = new Validator(formPersonalInformation);
25
26 function Validator(form) {
27 this.validatingForm = form;
28 form.addEventListener("submit", function() {
29 if (!validator.validate(this)) {
30 event.preventDefault();
31 event.returnValue = false;
32 return false;
33 }
34 alert("Success to validate");
35 return true;
36 });
37 }
38
39 Validator.prototype.ruleSet = {};
40 Validator.prototype.decorate = function(ruleName, ruleFunction) {
41 this.ruleSet[ruleName] = ruleFunction;
42 }
43 Validator.prototype.validate = function(form) {
44 var validatingForm = form || this.validatingForm,
45 inputs = validatingForm.getElementsByClassName("validate"),
46 length = inputs.length,
47 i, j,
48 input,
49 checkRules,
50 rule,
51 ruleLength;
52
53 for(i = 0; i<length; i++) {
54 input = inputs[i];
55 if (input.dataset.validateRules) {
56 checkRules = input.dataset.validateRules.split(" ");
57 ruleLength = checkRules.length;
58 for(j = 0; j<ruleLength; j++) {
59 rule = checkRules[j];
60 if (this.ruleSet.hasOwnProperty(rule)) {
61 if (!this.ruleSet[rule].call(null, input)) {
62 return false;
63 }
64 }
65 }
66 }
67 }
68
69 return true;
70 }
71 validator.decorate("required", function (input) {
72 if (!input.value) {
73 alert(input.name + " is required");
74 return false;
75 }
76 return true;
77 });
78 validator.decorate("alphabet", function (input) {
79 var regex = /^[a-zA-Z\s]*$/;
80 if (!regex.test(input.value)) {
81 alert(input.name + " has to be only alphabets");
82 return false;
83 }
84 return true;
85 });
86 validator.decorate("number", function (input) {
87 var regex = /^[0-9]{1,}$/;
88 if(!regex.test(input.value)) {
89 alert(input.name + " has to be only numbers");
90 return false;
91 }
92 return true;
93 });
94 })();
95</script>
96</body>
97</html>

Init-time branching 패턴

  • 초기화 단계에서 기능에 따라 다르게 호출되도록 함수가 변경되는 것.
  • 크로스 브라우저 할 때 주로 사용된다. ex) IE8 이하는 addEventListener 가 아닌 attachEvent 를 사용한다.

Self-defining function 패턴

  • Init-time branching 패턴과 유사한 패턴으로 Init-time branching 패턴이 최초 초기화 단계에 함수 호출 방법을 결정한다면, Self-defining function 패턴은 최초 실행되는 시기에 함수 호출 방법을 결정한다.
  • 처음 초기화 단계를 거치고 나면 이후에 같은 작업을 계속 반복하지 않아도 되거나 작업중에 또 요청이 왔을 때 중복실행을 방지할 경우 좋다.
1<!DOCTYPE html>
2<html>
3<head>
4 <meta charset="utf-8">
5 <meta name="viewport" content="width=device-width">
6 <title>Self-defining function 패턴</title>
7 <style>
8 #commentWrapper {
9 width: 200px;
10 }
11 .comment {
12 width: 150px;
13 display: inline-block;
14 }
15 .name {
16 width: 40px;
17 display: inline-block;
18 }
19 </style>
20</head>
21<body>
22 <div id="commentWrapper">
23 <div>
24 <div class="comment">Comment</div>
25 <div class="name">Name</div>
26 </div>
27 </div>
28 <form id="formComment">
29 <label>Comment: <input type="text" id="comment"></label>
30 <label>Name: <input type="text" id="name"></label>
31 <input type="submit">
32 </form>
33<script>
34 (function() {
35 var addComment = function() {
36 var divCommentWrapper = document.getElementById("commentWrapper"),
37 divCommentRow = document.createElement("div"),
38 divComment = document.createElement("div"),
39 divName = document.createElement("div"),
40 inputComment = document.getElementById("comment"),
41 inputName = document.getElementById("name");
42
43 divComment.className = "comment";
44 divName.className = "name";
45 divCommentRow.appendChild(divComment);
46 divCommentRow.appendChild(divName);
47
48 addComment = function() {
49 divComment.innerHTML = inputComment.value;
50 divName.innerHTML = inputName.value;
51
52 inputComment.value = "";
53 inputName.value = "";
54
55 divCommentWrapper.appendChild(divCommentRow.cloneNode(true));
56 };
57 addComment();
58 }
59
60 document.getElementById("formComment").addEventListener("submit", function() {
61 addComment();
62 event.returnValue = false;
63 return false;
64 });
65 })();
66</script>
67</body>
68</html>
  • 초기화 작업이 매번 필요한 함수나 클로저에 초기화 내용을 저장해야 하는 함수에 활용하면 좋다.
  • 단점은 불필요한 클로저가 생성되고 데이터가 메모리에 계속 남아있기 때문에 한 번만 사용하고 안 할 것이라면 undefined 로 초기화 하는 것이 좋다.

메모이제이션 패턴

  • 재귀가 아닌 일반적인 산술처리나 XMLHttpRequest 등과 같이 캐시를 활용할 수 있는 함수에 추가 기능으로 사용하면 좋다.
  • 재귀함수는 직접 메모이제이션 패턴을 설계하여 사용하는 것이 더 좋은 성능을 보여준다.
  • 캐시와 같은 기능을 수행하여 성능과 서버 요청 최적화에 유용하다.

Self-invoking constructor 패턴

  • new 를 빼 먹고 생성자를 호출할 경우를 대비해서 생성자를 그냥 함수로 호출할 때 스스로 new 를 붙여서 다시 호출하는 패턴.
  • 함수 기반의 객체지향을 개발하는 경우 방어적인 코딩으로 활용하면 좋다.

콜백 패턴

  • nodeJS 나 이벤트 핸들러의 콜백함수를 가리킨다.
  • 비동기 요청에 대한 순서 보장에 활용하면 좋다.

커링 패턴

  • 함수를 설계할 때 인자 전체를 넘겨서 호출할 수도 있지만, 일부 인자는 먼저 입력해두고 나머지만 입력받을 수 있도록 새로운 함수를 만드는 패턴
  • 자바스크립트는 클로저가 있어서 먼저 일부 입력된 값을 유지하고, 가지고 있는 것을 아주 쉽게 구현할 수 있다.
1(function () {
2 function sum(x, y) {
3 return x + y;
4 }
5
6 var makeAdder = function (x) {
7 return function (y) {
8 return sum(x, y);
9 };
10 };
11
12 var adderFour = makeAdder(4);
13 console.log(adderFour(1));
14 console.log(adderFour(5));
15})();
1(function () {
2 Function.prototype.curry = function() {
3 if (arguments.length < 1) {
4 return this;
5 }
6
7 var _this = this,
8 args = Array.prototype.slice.apply(arguments);
9
10 return function() {
11 return _this.apply(this, args.concat(Array.prototype.slice.apply(arguments)));
12 }
13 }
14
15 function unitConvert(fromUnit, toUnit, factor, input) {
16 return input + ' ' + fromUnit + ' = ' + (input * factor).toFixed(2) + ' ' + toUnit;
17 }
18
19 var cm2inch = unitConvert.curry('cm', 'inch', 0.393701),
20 metersquare2pyoung = unitConvert.curry('m^2', 'pyoung', 0.3025),
21 kg2lb = unitConvert.curry('kg', 'lb', 2.204623),
22 kmph2mph = unitConvert.curry('km/h', 'mph', 0.621371);
23
24 console.log(cm2inch(10));
25 console.log(metersquare2pyoung(30));
26 console.log(kg2lb(50));
27 console.log(kmph2mph(100));
28})();
  • 커링 패턴은 하나의 공용 함수가 있는 경우 이를 세부적인 기능을 하는 함수로 나누고 싶을 때 유용하다.
  • 단점은 클로저도 그렇지만 프로그램이 돌아가는 순서를 쫓아가기에 조금 힘들 수도 있다.

브라우저 환경에서의 자바스크립트

requestAnimationFrame()

  • 시간을 보장하는 함수
  • setInterval()은 백그라운드에서도 실행되지만 requestAnimationFrame() 은 화면에 repaint 가 일어날 때 호출되므로 백그라운드에서 호출되지 않고 대기한다.

DOM repaint

  • DOM 요소들의 위치가 변경되지 않고 표현되는 스타일만 변경되는 경우 다시 그리는 기능

DOM reflow

  • DOM이 화면에 표시되는 구조가 바뀔 때, 또는 CSS 클래스가 바뀔 때 위치를 다시 계산하는 기능
  • 특정 element가 변경되면 그에 영향을 받는 자식 element와 해당 element 이후에 나타나는 element들에 대해서 전체적으로 위치를 다시 계산해야 하므로 자원소모가 repaint 보다 크다.
  • element의 크기가 변경되거나 className이 변경되거나 브라우저 창의 크기가 변경될 때 발생한다.

reflow 자원 소모 최소화 방법

  • 크기가 변경될 DOM의 자식 element 를 최소화하거나 변경될 DOM을 웹페이지의 아래쪽에 배치하는 것.
  • 크기나 위치의 변경이 일어나는 DOM element 스타일의 position 을 absolutefixed 로 설정하는 것이 좋다.
  • 위치가 변경되는 element에 자식 element를 너무 많이 만들지 않는 것이 기본.
  • position:relative 안에 position:absolute를 이용하면 페이지 중간에도 position:absolute 를 넣을 수 있다.
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <meta http-equiv="X-UA-Compatible" content="ie=edge">
7 <title>position:relative 와 position:absolute</title>
8 <style>
9 #bannerWrapper {
10 overflow: hidden;
11 height: 400px;
12 width: 265px;
13 position: relative;
14 }
15
16 #bannerImg {
17 position: absolute;
18 }
19
20 #bannerImg.mouseover {
21 top: -400px;
22 }
23 </style>
24</head>
25<body>
26 <div id="bannerWrapper">
27 <img id="bannerImg" src="http://i.imgur.com/3DpJ0ou.png"/>
28 </div>
29 <script>
30 (function() {
31 var imgBannerImg = document.getElementById('bannerImg');
32 imgBannerImg.addEventListener('mouseover', function() {
33 this.classList.add('mouseover');
34 });
35 imgBannerImg.addEventListener('mouseout', function() {
36 this.classList.remove('mouseover');
37 });
38 })();
39 </script>
40</body>
41</html>
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <meta http-equiv="X-UA-Compatible" content="ie=edge">
7 <title>DOM reflow를 최소화한 공지사항 목록</title>
8 <style>
9 #noticeWrapper {
10 overflow: hidden;
11 height: 20px;
12 width: 200px;
13 position: relative;
14 border: 1px solid black;
15 }
16
17 #notice {
18 position: absolute;
19 margin: 0;
20 padding: 0;
21 top: 0;
22 }
23
24 .noticeSubject {
25 height: 20px;
26 width: 200px;
27 list-style: none;
28 }
29
30 .noReflow {
31 width: 200px;
32 height: 100px;
33 border: 1px solid black;
34 background-color: lightgray;
35 }
36 </style>
37</head>
38<body>
39 <div class="noReflow">
40 I'll nor reflow
41 </div>
42 <div id="noticeWrapper">
43 <ul id="notice">
44 <li class="noticeSubject">Link to the first article</li>
45 <li class="noticeSubject">Link to the second article</li>
46 <li class="noticeSubject">Link to the third article</li>
47 <li class="noticeSubject">Link to the last article</li>
48 </ul>
49 </div>
50 <div class="noReflow">
51 Neither me
52 </div>
53 <script>
54 (function() {
55 var ulNotice = document.getElementById('notice'),
56 currentNoticeTop = 0,
57 currentIndex = 0,
58 maxIndex = ulNotice.getElementsByClassName('noticeSubject').length - 1,
59 currentRollingUp = true,
60 subjectHeight = 20,
61 velocityPerSecond = 20,
62 previousFrame = null;
63
64 setTimeout(rollNextNotice, 0);
65
66 function rollNextNotice() {
67 requestAnimationFrame(rollNotice);
68 }
69
70 function rollNotice(time) {
71 var diff = (previousFrame !== null ? time - previousFrame : 0);
72 previousFrame = time;
73 currentNoticeTop += (diff / 1000) * velocityPerSecond;
74
75 if (currentNoticeTop * velocityPerSecond >= currentIndex * -subjectHeight * velocityPerSecond) {
76 if (currentIndex === maxIndex || currentIndex === 0) {
77 currentRollingUp = !currentRollingUp;
78 velocityPerSecond = -velocityPerSecond;
79 }
80
81 currentNoticeTop = currentIndex * -subjectHeight;
82 currentIndex += (currentRollingUp ? -1 : 1);
83 previousFrame = null;
84 setTimeout(rollNextNotice, 1000);
85 } else {
86 requestAnimationFrame(rollNotice);
87 }
88 ulNotice.style.top = currentNoticeTop + 'px';
89 }
90 })();
91 </script>
92</body>
93</html>
  • reflow 가 일어 날 때 CSS 선택자에 대한 계산도 다시 일어나는데, 일반적으로 CSS에서 많은 자원 소모가 있는 것은 단계적으로 태그명을 토대로 상화 관계를 명시해서 스타일을 적용한 경우다.
    • 이를 방지하기 위해 ID나 class를 선택자로 이용해서 DOM 탐색을 최소화하는 것이 좋다.
  • DOM 을 추가/삭제 시 reflow 가 여러 번 발생 할 수 있다. 이를 방지하기 위해 화면 뒤에서 DOM이 안 보이는 상태에서 설정할 수 있는 모든 내용을 설정한 다음에 화면에 표시되도록 하는 것이 유리하다.
    • 여러 개의 DOM을 추가 할 때는 DocumentFragment 객체에 추가해두고 마지막에 실제 DOM에 추가하는 것이 좋다.
    • 또는 display:none 으로 Element 를 아예 백그라운드로 뺀 다음 변경 내용을 반영한 후 다시 display:block 을 하면 좋다.
  • DOM element 의 offsetWidthclientHeight에 접근하거나 getComputedStyle() 에 접근할 때 reflow 가 일어날 수 있다.
    • 일반적으로 브라우저는 실행중인 자바스크립트가 끝난 후 reflow 를 한번에 처리하지만, 이 경우 자바스크립트가 실행중에 진행되므로 성능에 치명적일 수 있다.
    • 될 수 있으면 DOM reflow 를 일으키는 모든 변경을 완료하고 속성에 접근하는 것이 좋다.

DOM 탐색 횟수 최소화

  • DOM 을 탐색하는 대표적인 함수는 다음과 같다.
1var element = document.getElementById('myElement');
2var classElements = document.getElementsByClassName('myClass');
3var tagElements = document.getElementsByTagName('div');
4var cssSelector = document.querySelector('div#cssSelector');
5var cssSelectors = document.querySelectorAll('div.myClass');
  • DOM 을 탐색해서 가져오는 것은 자바스크립트 내에서 클로저 내의 변수를 검색하거나 속성을 검색하는 것보다 컴퓨팅 자원을 더 소모한다.
  • 메모이제이션 패턴을 사용해서 DOM 탐색비용을 줄일 수도 있다.
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <meta http-equiv="X-UA-Compatible" content="ie=edge">
7 <title>메모이제이션 패턴을 이용한 DOM 탐색 최적화 예</title>
8</head>
9<body>
10 <form id="myForm">
11 <input type="text" name="firstName" id="firstName"/>
12 <input type="text" name="middleName" id="middleName"/>
13 <input type="text" name="lastName" id="lastName"/>
14 <input type="submit" value="Submit"/>
15 </form>
16 <script>
17 (function() {
18 var memo = {};
19 document.getElementById('myForm').onsubmit = function() {
20 var inputFirstName = getMemo('firstName'),
21 inputMiddleName,
22 inputLastName;
23
24 if (inputFirstName.value === '') {
25 alert('First name is mandatory');
26 inputFirstName.focus();
27 return false;
28 }
29
30 inputLastName = getMemo('lastName');
31
32 if (inputLastName.value === '') {
33 alert('Last name is mandatory');
34 inputLastName.focus();
35 return false;
36 }
37
38 inputMiddleName = getMemo('middleName');
39
40 alert('Hello, ' + inputFirstName.value + ' ' + inputMiddleName.value + ' ' + inputLastName.value);
41 }
42
43 function getMemo(id) {
44 memo[id] = memo[id] || document.getElementById(id);
45
46 return memo[id];
47 }
48 })();
49 </script>
50</body>
51</html>

웹 워커

  • 싱글스레드인 브라우저에서 별도의 자바스크립트 스레드가 생김 이것이 웹 워커
  • 별도의 스레드라 UI를 직접 변경시킬순 없다.
  • dedicated worker 는 worker를 생성한 스크립트에서만 호출과 접근을 할 수 있고, shared worker는 여러 개의 스크립트에서 접근할 수 있다.
  • 하지만 shared workerconsole.log() 등의 함수들이 브라우저 개발자 도구를 통해서 확인할 수 없으므로 디버깅하기 어렵다.
1<!-- index.html -->
2<!DOCTYPE html>
3<html lang="en">
4<head>
5 <meta charset="UTF-8">
6 <meta name="viewport" content="width=device-width, initial-scale=1.0">
7 <meta http-equiv="X-UA-Compatible" content="ie=edge">
8 <title>웹 워커 사용 예</title>
9</head>
10<body>
11 <select id="doWorker">
12 <option>-- SELECT --</option>
13 <option>doLargeLoop</option>
14 </select>
15 <script>
16 (function() {
17 var worker = new Worker('./worker.js'),
18 selectWorker = document.getElementById('doWorker');
19
20 selectWorker.addEventListener('change', function() {
21 console.log('main thread: sending message - ' + this.value);
22 worker.postMessage(this.value);
23 });
24
25 worker.onmessage = function (msg) {
26 console.log('main thread: ' + msg.data);
27 }
28 })();
29 </script>
30</body>
31</html>
1// worker.js
2(function () {
3 var messages = {
4 'doLargeLoop': function() {
5 var i, sum = 0,
6 start, end;
7 console.log('worker thread: starting large loop');
8
9 start = Date.now();
10 for (i = 0; i < 10000000000; i++) {
11 sum += i;
12 }
13
14 end = Date.now();
15 postMessage(`Elapsed time (${((end - start) / 1000).toFixed(2)} sec, sum=${sum}`);
16 }
17 };
18
19 onmessage = function (msg) {
20 if (messages.hasOwnProperty(msg.data)) {
21 messages[msg.data]();
22 }
23 }
24})();

자바스크립트 성능과 사용자 경험 개선

  • 브라우저는 DOM 을 전체적으로 파싱하면서 화면 레이아웃에 영향을 미치는 모든 CSS태그, 파일들을 파싱한다. 그 다음 화면에 표시하기 위해 레이아웃을 잡고 렌더링 단계를 거친다.
  • <img> 태그의 크기가 변하면 이미지가 로드될때마다 DOM reflow 가 일어나서 전체적인 웹페이지의 로딩 속도가 느려진다.
  • HTTP GET 요청을 최소화 하기 위해 gzip 압축, Expires 헤더를 통한 캐싱 등이 있다.
© 2020 by Kyunghwa Yoo.