|
| 1 | +클로저는 중요한 개념입니다. 비단 자바스크립트 뿐만 아닌, 함수를 일급 객체로 취급하는 함수형 프로그래밍 언어에서 사용되기 때문이죠. 클로저는 단적으로 말하자면, `함수가 선언됐을 때의 렉시컬 환경(Lexical environment)`입니다. |
| 2 | + |
| 3 | +## 렉시컬 스코프 |
| 4 | +> 자바스크립트 엔진은 함수를 어디에 정의했는지에 따라 상위 스코프를 결정하며, 이를 렉시컬 스코프(정적 스코프)라고 합니다. |
| 5 | +
|
| 6 | +이미 [스코프](https://github.com/FECrash/JavascriptCrash/blob/mainner/Javascript/scope.md)에서 다룬 내용입니다. 아래 코드를 볼까요? |
| 7 | + |
| 8 | +```js |
| 9 | +const x = 1; |
| 10 | + |
| 11 | +function outer(){ |
| 12 | + const x = 10; |
| 13 | + inner(); |
| 14 | +} |
| 15 | + |
| 16 | +function inner(){ |
| 17 | + console.log(x); |
| 18 | +} |
| 19 | + |
| 20 | +outer(); // 1 |
| 21 | +inner(); // 2 |
| 22 | +``` |
| 23 | + |
| 24 | +outer와 inner 함수의 상위 스코프는 전역입니다. 함수를 어디에서 호출했는지는 함수의 상위 스코프에 어떤 영향도 미치지 않습니다. 즉, 정적으로 결정되고 변하지 않는다는 것이죠. 스코프의 실체는 실행 컨텍스트의 렉시컬 환경이며, 렉시컬 환경은 자신의 `외부 렉시컬 환경에 대한 참조`를 통해 상위 렉시컬 환경과 연결되는 이것이 **스코프 체인(Scope Chainner)** 입니다. |
| 25 | + |
| 26 | +결국 **렉시컬 스코프는 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에 저장할 참조 값(상위 스코프에 대한 참조)은 함수가 평가되는 시점에 함수가 정의된 환경(위치)에 의해 결정되는 것**입니다. |
| 27 | + |
| 28 | +<br> |
| 29 | + |
| 30 | +## 함수 객체의 내부 슬롯 `[[Environment]]` |
| 31 | +> 함수는 자신의 내부 슬롯 `[[Environment]]`에 자신이 정의된 환경인 상위 스코프의 참조를 저장합니다. |
| 32 | +
|
| 33 | +함수가 정의된 환경(위치)과 호출되는 환경(위치)는 다를 수 있습니다. 따라서 렉시컬 스코프는 자신이 정의된 환경에서의 상위 스코프를 기억해야 하는데, 이 때 자신의 내부 슬롯인 `[[Environment]]`에 상위 스코프의참조를 저장하고, 이는 현재 실행 중인 실행 컨텍스트의 렉시컬 환경을 가리키게 됩니다. |
| 34 | + |
| 35 | +그 이유는 함수 정의가 평가되어 함수 객체를 생성하는 시점은 상위 함수(또는 전역 코드)가 평가되고 실행되는 시점이고, 실행 컨텍스트는 상위 함수(또는 전역 코드)이기 때문이죠. 결국 함수 객체의 내부 슬롯 `[[Environment]]`에 저장된 현재 실행 중인 실행 컨텍스트의 렉시컬 환경의 참조가 바로 상위 스코프가 되는 것입니다. |
| 36 | + |
| 37 | +함수가 호출되면 함수 내부로 코드의 제어권(Control)이 이동하고, 함수를 아래의 순서대로 평가합니다. |
| 38 | +1. 함수 실행 컨텍스트 생성 |
| 39 | +2. 함수 렉시컬 환경 생성 |
| 40 | + 1. 함수 환경 레코드 생성 |
| 41 | + 2. this 바인딩 |
| 42 | + 3. `외부 렉시컬 환경에 대함 참조 결정` |
| 43 | + |
| 44 | +외부 렉시컬 환경에 대한 참조에는 함수 객체 내부 슬롯 `[[Environment]]`에 저장된 렉시컬 환경의 참조가 할당되고, 이는 함수의 상위 스코프를 의미합니다. 이것이 바로 함수 정의 위치에 따라 상위 스코프를 결정하는 렉시컬 스코프입니다. 자, 여기까지 고생하셨어요. 이제 클로저를 알아봅시다. |
| 45 | + |
| 46 | +<br> |
| 47 | + |
| 48 | +## 클로저와 렉시컬 환경 |
| 49 | +> 외부 함수보다 더 오래 유지되어 외부 함수의 변수를 참조할 수 있는 내부 함수를 클로저(Closure)라고 부릅니다. |
| 50 | +
|
| 51 | +코드의 흐름대로 이해해보죠. 효과적인 이해를 위해 함수 표현식으로 작성했습니다. |
| 52 | +```js |
| 53 | +const x = 1; |
| 54 | + |
| 55 | +function outer(){ |
| 56 | + const x = 10; |
| 57 | + return function() { |
| 58 | + console.log(x); |
| 59 | + }; |
| 60 | +} |
| 61 | + |
| 62 | +const exec = outer(); // 함수 호출 |
| 63 | +exec(); // 10 |
| 64 | +``` |
| 65 | + |
| 66 | +1. `const exec = outer();`로 outer 함수를 호출하면 익명함수를 반환하고 outer 함수는 종료되어 실행 컨텍스트 스택에서 제거(pop)됩니다. |
| 67 | +2. outer 함수의 실행 컨텍스트가 제거되었으므로 지역 변수 `const x = 10;`은 당연히 종료됩니다. |
| 68 | +3. 유효하지 않게 된 지역 변수 x의 값이므로, 실행 결과는 1이 되어야 한다고 저희는 **생각**합니다. |
| 69 | + |
| 70 | +그러나 실행 결과는 outer 함수의 지역 변수 x의 값인 `10`이 출력됩니다. 이처럼 **내부 함수가 외부함수보다 오래 유지되는 경우, 내부 함수는 종료된 외부 함수의 변수를 참조할 수 있게 되고 이를 클로저라 부릅니다**. |
| 71 | + |
| 72 | +outer 함수가 호출되어 내부 함수가 평가되면 반환값으로 정의된 익명 함수는 자신의 내부 슬롯 `[[Environment]]`에 현재 실행 중인 실행 컨텍스트의 렉시컬 환경인 outer 함수의 렉시컬 환경을 상위 스코프로 지정합니다. |
| 73 | + |
| 74 | +outer 함수의 실행이 종료된어 실행 컨텍스트 스택이 제거된다고 outer 함수의 렉시컬 환경까지 소멸하는 것이 아니에요. 이를 설명하기 전에, 현재 자바스크립트 엔진은 [표시하고-쓸기(Mark-and-sweep) 알고리즘](https://developer.mozilla.org/ko/docs/Web/JavaScript/Memory_Management#mark-and-sweep_algorithm)을 사용한 Garbage Collection을 사용하고 있는 것을 알아두시면 이해하기 쉬워집니다. |
| 75 | + |
| 76 | +정리하자면, outer 함수의 렉시컬 환경은 현재 반환된 익명 함수의 내부 슬롯 `[[Environment]]`이 참조하고 있으며, 이는 전역 변수 exec에 할당했으므로 Garbage Collection의 대상이 되지 않는 것입니다. Garbage Collector는 참조되고 있는 메모리 공간을 마음대로 제거하지 않는 것이죠! |
| 77 | + |
| 78 | +자바스크립트의 모든 함수는 상위 스코프를 기억하기 때문에 이론적으로는 모두 클로저입니다. 그러나 일반적으로는 모든 함수를 클로저라고 하진 않죠. 다음 예제를 볼까요? |
| 79 | +```js |
| 80 | +const x = 1; |
| 81 | + |
| 82 | +function outer(){ |
| 83 | + const y = 10; // * 이 부분만 다릅니다. |
| 84 | + return function() { |
| 85 | + console.log(x); |
| 86 | + }; |
| 87 | +} |
| 88 | + |
| 89 | +const exec = outer(); // 함수 호출 |
| 90 | +exec(); // 1 |
| 91 | +``` |
| 92 | + |
| 93 | +예제를 이해하기 전에 생각해야 할 것이 있습니다. 모든 상위 스코프를 기억하려면 브라우저의 메모리 성능이 매우 저하되겠죠? 자, 위의 예제에서도 반환된 익명 함수는 외부 함수보다 오래 유지됩니다. 그러나 상위 스코프의 y 변수를 참조하지 않으므로 반환된 익명 함수의 참조는 곧바로 소멸합니다. 이처럼 대부분의 모던 브라우저는 최적화를 통해 상위 스코프의 어떤 식별자도 참조하지 않는 경우 상위 스코프를 기억하지 않습니다. 모던 자바스크립트 엔진은 최적화가 매우 잘 되어 있어요. 클로저가 참조하고 있지 않은 식별자는 기억하지 않습니다. 기억해야 할 식별자만을 기억한다는 것은 메모리 낭비라고 할 수 없죠. |
| 94 | + |
| 95 | +여담으로 클로저에 의해 참조되는 상위 스코프의 변수를 `자유 변수(Free Variable)`라 부르며 클로저를 `자유 변수에 묶여있는 함수`라고 의역할 수 있습니다. |
| 96 | + |
| 97 | +<br> |
| 98 | + |
| 99 | +## 클로저의 활용 |
| 100 | +> 상태(State)를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하기 위해 사용합니다. |
| 101 | +
|
| 102 | +캡슐화(Encapsulation)는 객체의 상태를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작하는 동작인 메서드를 하나로 묶는 것입니다. 마치 캡슐처럼 쓰기 편해지죠. 여기에는 특정 프로퍼티, 메서드를 숨길 목적으로 사용하기도 하며 이를 정보 은닉(Information Hiding)이라고 합니다. 정보를 숨긴다는 것은 구현한 객체의 내부를 공개하지 않으므로 외부의 옳지 못한 접근으로부터 객체의 상태가 변경되는 것을 방지합니다. 이는 정보를 보호하고 객체 간 상호 의존성(결합도, Coupling)을 낮추는 효과가 있죠. |
| 103 | + |
| 104 | +그러나 자바스크립트는 정보 은닉을 완전하게 지원하지 않아서... 아래와 같이 흉내만 낼 수 있죠. 이럴 때 클로저가 매우 유용하게 사용됩니다. |
| 105 | + |
| 106 | +```js |
| 107 | +// 함수를 인수로 전달받고 함수를 반환하는 고차 함수 |
| 108 | +// 자유 변수 counter를 기억하는 클로저를 반환합니다. |
| 109 | +function makeCounter(predicate) { |
| 110 | + // 카운트 상태를 유지하는 자유 변수 |
| 111 | + let counter = 0; |
| 112 | + |
| 113 | + // 클로저 반환 |
| 114 | + return function () { |
| 115 | + // 인수로 전달 받은 보조 함수에 상태 변경 위임 |
| 116 | + counter = predicate(counter); |
| 117 | + return counter; |
| 118 | + }; |
| 119 | +} |
| 120 | + |
| 121 | +// 보조 함수 |
| 122 | +function increase(n) { |
| 123 | + return ++n; |
| 124 | +} |
| 125 | + |
| 126 | +// 보조 함수 |
| 127 | +function decrease(n) { |
| 128 | + return --n; |
| 129 | +} |
| 130 | + |
| 131 | +// 함수로 함수를 생성합니다. |
| 132 | +// makeCounter 함수는 보조 함수를 인수로 전달받아 함수를 반환합니다. |
| 133 | +const increaser = makeCounter(increase); |
| 134 | +console.log(increaser()); // 1 |
| 135 | +console.log(increaser()); // 2 |
| 136 | + |
| 137 | +// increaser 함수와는 독립된 렉시컬 환경이므로 내부 상태가 연동되지 않습니다. |
| 138 | +const decreaser = makeCounter(decrease); |
| 139 | +console.log(decreaser()); // -1 |
| 140 | +console.log(decreaser()); // -2 |
| 141 | +``` |
| 142 | + |
| 143 | +makeCounter 함수는 고차 함수입니다. 이 함수가 반환하는 것은 자신이 생성된 렉시컬 환경인 makeCounter 함수 스코프의 counter 변수를 기억하는 클로저죠. 코드로 보면 결과가 이어지지 않죠? 이는 makeCounter 함수를 호출하여 반환된 함수는 **자신만의 독립된 렉시컬 환경**을 갖는 것입니다. 즉, 함수를 호출할 때마다 새로운 makCounter 함수의 실행 컨텍스트의 렉시컬 환경이 생성되는 것이죠. |
| 144 | + |
| 145 | +이처럼 클로저는 상태(State)가 의도치 않게 변경되지 않도록 안전하게 정보를 은닉하고 특정 함수에게만 상태 변경을 허용할 수 있습니다. |
| 146 | + |
| 147 | +만약 위의 예제에서 상태를 **공유(Share)** 하고 싶다면 아래와 같이 작성해봅니다. |
| 148 | +```js |
| 149 | +// 함수를 반환하는 고차 함수 |
| 150 | +// 자유 변수 counter를 기억하는 클로저를 반환합니다. |
| 151 | +const counter = (function () { |
| 152 | + // 카운트 상태를 유지하기 위한 자유 변수 |
| 153 | + let counter = 0; |
| 154 | + |
| 155 | + // 함수를 인수로 전달받는 클로저 반환 |
| 156 | + return function (predicate) { |
| 157 | + // 인수로 전달 받은 보조 함수에 상태 변경을 위임합니다. |
| 158 | + counter = predicate(counter); |
| 159 | + return counter; |
| 160 | + }; |
| 161 | +}()); |
| 162 | + |
| 163 | +// 보조 함수 |
| 164 | +function increase(n) { |
| 165 | + return ++n; |
| 166 | +} |
| 167 | + |
| 168 | +// 보조 함수 |
| 169 | +function decrease(n) { |
| 170 | + return --n; |
| 171 | +} |
| 172 | + |
| 173 | +// 보조 함수를 전달하여 호출합니다. |
| 174 | +console.log(counter(increase)); // 1 |
| 175 | +console.log(counter(increase)); // 2 |
| 176 | + |
| 177 | +// 자유 변수를 공유합니다. |
| 178 | +console.log(counter(decrease)); // 1 |
| 179 | +console.log(counter(decrease)); // 0 |
| 180 | +``` |
| 181 | + |
| 182 | +독립된 카운터가 아니라 연동하여 증감이 가능한 카운터를 만들려면 위의 코드처럼 **렉시컬 환경을 공유하는 클로저를 만들어야** 합니다. |
| 183 | + |
| 184 | +<br> |
| 185 | +<hr> |
| 186 | +<br> |
| 187 | + |
| 188 | +> 2021-10-04, 추가적으로 공부와 정리가 필요한 내용들 |
| 189 | +
|
| 190 | +## 🧐 클로저를 사용할 때 주의할 점 |
| 191 | +### 의문점 |
| 192 | +- 자바스크립트의 렉시컬 환경(Lexical Environment)는 **선언된 시점**에서 스코프를 갖는데, `this`는... 다른가요? 아래 예제 코드를 보죠! |
| 193 | + |
| 194 | +### 정리 된 내용 |
| 195 | +- *정리해야함* |
| 196 | + |
| 197 | +- 참조 [Link](https://www.hanumoka.net/2017/08/31/javascript-20170831-javascript-closure-3/) |
| 198 | + |
| 199 | +<hr> |
| 200 | +<br> |
0 commit comments