Skip to content

Commit a6e44ca

Browse files
committed
feat: 승현-클로저.md
1 parent 95a5fc1 commit a6e44ca

File tree

1 file changed

+281
-0
lines changed

1 file changed

+281
-0
lines changed

Diff for: week07_클로저/승현-클로저.md

+281
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
# 24장 클로저
2+
3+
## 24.4 클로저의 활용
4+
5+
> 클로저는 상태를 의도치 않게 변경되지 않도록 상태를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하기 위해 사용됩니다.
6+
7+
8+
아래 간단한 예제 코드를 통해 안전하게 유지해야 할 상태에 대해서 알아보겠습니다.
9+
```js
10+
// 카운트 상태 변수
11+
let num = 0;
12+
13+
// 카운트 상태 변경 함수
14+
const increase = function() {
15+
// 카운트 상태를 1만큼 증가 시킵니다.
16+
return ++num;
17+
}
18+
19+
console.log(increase()); // 1
20+
console.log(increase()); // 2
21+
console.log(increase()); // 3
22+
```
23+
위 코드는 오류를 발생시킬 가능성을 내포하고 있는 좋지 않은 코드입니다.
24+
25+
이유가 무엇일까요?!
26+
1. 카운트 상태는 increase 함수가 호출되기 전까지 변경되지 않고 유지되어야 합니다.
27+
2. 이를 위해 카운트 상태는 increase 함수만이 변경할 수 있어야 합니다.
28+
29+
하지만 카운트 상태는 전역 변수를 통해 관리되고 있기 때문에 언제든지 변수에 접근할 수 있고 변경할 수 있습니다. 이는 의도치 않게 상태가 변경될 수 있다는 것을 의미합니다. 이는 예상치 못한 오류로 이어질 수 있습니다.
30+
31+
<br>
32+
33+
이번에는 위에서 발생한 문제를 해결할 수 있는(의도치 않는 상태 변경을 방지할 수 있는) 예제 코드를 살펴보겠습니다.
34+
```js
35+
// 카운트 상태 변경 함수
36+
const increase = (function () {
37+
// 카운트 상태 변수
38+
let num = 0;
39+
40+
return function() {
41+
// 카운트 상태를 1만큼 증가 시킵니다.
42+
return ++num;
43+
}
44+
}())
45+
46+
console.log(increase()); // 1
47+
console.log(increase()); // 2
48+
console.log(increase()); // 3
49+
```
50+
1. 위 코드가 실행되면 increase 변수에 즉시 실행 함수가 실행되어 함수가 반환됩니다.
51+
2. increase 함수를 실행 시키면 반환된 함수가 실행됩니다. 이 함수는 자신이 정의된 위치에 의해 결정된 상위 스코프의 렉시컬 환경을 기억하는 클로저입니다.
52+
3. increase 함수를 실행 시키면 가장 먼저 함수 내부의 실행 컨텍스트에서 num 변수를 찾습니다. 하지만 내부에는 num이라는 변수가 존재하지 않기 때문에 상위 스코프의 렉시컬 환경을 참조합니다. 이렇게 상위 즉시 실행 함수의 환경 레코드에서 num을 찾아 해당 값을 1 증가 시키고 함수를 종료합니다.
53+
54+
즉시 실행 함수는 한 번만 실행되므로 increase가 호출될 때마다 num 변수가 초기화될 일은 없을 것입니다. 또한 num 변수는 외부에서는 직접 접근할 수 없는 은닉화 private 변수이므로 전역 변수를 사용했을 때와 같이 의도되지 않은 변경을 걱정할 필요도 없기 때문에 더 안정적인 프로그래밍이 가능합니다.
55+
56+
이처럼 **`클로저`는 상태가 의도치 않게 변경되지 않도록 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하여 상태를 안전하게 변경하고 유지하기 위해 사용됩니다.**
57+
58+
지금까지 `increase` 함수를 통해 카운트를 증가하는 코드를 살펴보았습니다.
59+
60+
<br>
61+
62+
이번에는 조금 더 발전 시켜 증가, 감소 모두 사용 가능하도록 만들어보겠습니다.
63+
```js
64+
const counter = (function () {
65+
// 카운트 상태 변수
66+
let num = 0;
67+
68+
// 클로저인 메서드를 갖는 객체를 반환합니다.
69+
// 객체 리터럴은 스코프를 만들지 않습니다.
70+
// 따라서 아래 메서드들의 상위 스코프는 즉시 실행 함수의 렉시컬 환경입니다.
71+
return {
72+
// num: 0, 프로퍼티는 public하므로 은닉되지 않는다.
73+
increase() {
74+
return ++num;
75+
},
76+
decrease() {
77+
return --num;
78+
}
79+
}
80+
}());
81+
82+
console.log(counter.increase()); // 1
83+
console.log(counter.increase()); // 2
84+
console.log(counter.decrease()); // 1
85+
console.log(counter.decrease()); // 0
86+
```
87+
1. 즉시 실행 함수가 반환하는 객체 메서드도 함수 객체로 생성됩니다.
88+
2. 객체 리터럴의 중괄호는 코드 블록이 아니므로 별도의 스코프를 생성하지 않습니다.
89+
3. `increase`, `decrease` 메서드의 상위 스코프는 메서드가 평가되는 시점에 실행중인 실행 컨텍스트인 즉시 실행 함수 컨텍스트의 렉시컬 환경입니다.
90+
91+
따라서, `increase`, `decrease` 메서드는 언제 어디서 호출되든 상관없이 즉시 실행 함수의 스코프 식별자를 참조할 수 있습니다.
92+
93+
### 함수형 프로그래밍
94+
다음은 함수형 프로그래밍에서 클로저를 활용하는 간단한 예제입니다.
95+
```javascript
96+
// 함수를 인수로 전달받고 함수를 반환하는 고차 함수
97+
// 이 함수는 카운트 상태를 유지하기 위한 변수 counter를 기억하는 클로저를 반환합니다.
98+
function makeCounter(aux) {
99+
// 카운트 상태를 유지하기 위한 변수
100+
let counter = 0;
101+
102+
// 클로저를 반환
103+
return function () {
104+
counter = aux(counter);
105+
return counter;
106+
};
107+
}
108+
109+
// 보조 함수
110+
function increase(n) {
111+
return ++n;
112+
}
113+
114+
function decrease(n) {
115+
return --n;
116+
}
117+
118+
// 함수로 함수를 생성한다.
119+
// makeCounter 함수는 보조 함수를 인수로 전달받아 함수를 반환한다.
120+
const increaser = makeCounter(increase) {
121+
console.log(increaser()); // 1
122+
console.log(increaser()); // 2
123+
}
124+
125+
const decreaser = makeCounter(decrease) {
126+
console.log(decreaser()); // -1
127+
console.log(decreaser()); // -2
128+
}
129+
```
130+
`makeCounter` 함수가 반환하는 함수는 자신이 생성됐을 때의 렉시컬 환경인 `makeCounter` 함수의 스코프에 속한 `counter` 변수를 기억하는 클로저 입니다.
131+
132+
이때 주의해야 할 것은 **`makeCounter` 함수를 호출해 함수를 반환할 때 반환된 함수는 자신만의 독립된 렉시컬 환경을 갖는다는 것입니다.**
133+
이는 함수를 호출하면 그때마다 새로운 maksCounter 함수 실행 컨텍스트의 렉시컬 환경이 생성되기 때문입니다.
134+
135+
![image](https://github.com/SeungHyune/study/assets/114329713/51c68f72-a013-478e-b455-a88f84dce160)
136+
위 예제에서 전역변수 increaser와 decreaser에 할당된 함수는 각각 자신만의 독립된 렉시컬 환경을 갖기 때문에 카운트를 유지하기 위한 자유 변수 counter를 공유하지 않아 카운터의 증감이 연동되지 않습니다.
137+
138+
이를 서로 연동된 증감 카운터를 만들려면 렉시컬 환경을 공유하는 클로저를 만들어야 합니다. (이를 위해서는 makeCounter 함수를 두 번 호출하지 말아야 합니다.)
139+
```javascript
140+
const counter = (function (){
141+
let counter = 0;
142+
143+
return function(aux) {
144+
aux(counter);
145+
return counter;
146+
}
147+
}());
148+
149+
function increase(n) {
150+
return ++n;
151+
}
152+
153+
function decrease(n) {
154+
return --n;
155+
}
156+
157+
// 증가
158+
console.log(counter(increase)); 1
159+
console.log(counter(increase)); 2
160+
161+
// 감소
162+
console.log(counter(decrease)); 1
163+
console.log(counter(decrease)); 0
164+
```
165+
1. `counter` 변수에 즉시 실행 함수의 반환 함수를 저장합니다. 이 함수는 자신의 상위 스코프인 즉시 실행 함수의 렉시컬 환경을 기억하는 클로저 입니다.
166+
2. 반환 받은 함수에 증가, 감소 함수를 인자로 전달하여 counter 함수가 기억하는 `counter` 변수의 값의 상태를 변경할 수 있습니다.
167+
3. 이때, 이전 코드와는 다르게 `increase`, `decrease` 함수는 `counter` 변수를 공유할 수 있습니다.
168+
169+
<br>
170+
171+
## 24.5 캡슐화 정보 은닉
172+
`캡슐화` 는 객체의 상태를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 메서드를 하나로 묶는 것을 말합니다.
173+
174+
캡슐화는 객체의 특징을 프로퍼티나 메서드를 감출 목적으로 사용하기도 하는데 이를 `정보 은닉` 이라 합니다.
175+
176+
자바스크립트는 `public`, `private`, `protexted` 같은 접근 제한자를 제공하지 않습니다. 기본적으로 모든 객체 프로퍼티, 메서드는 외부에 공개되어 있습니다. (기본적으로 public이다)
177+
178+
```javascript
179+
const Person = (function(){
180+
let _age = 0;
181+
182+
// 생성자 함수
183+
function Person(name, age) {
184+
this.name = name;
185+
_age =age;
186+
}
187+
188+
// 프로토타입 메서드
189+
Person.prototype.sayHi = function() {
190+
console.log(`Hi! My name is ${this.name}. I am ${_age}`);
191+
}
192+
193+
// 생성자 함수 반환
194+
return Person
195+
}());
196+
197+
const me = new Person('Lee', 20);
198+
me.sayHi(); // Hi! My name is Lee. I am 20.
199+
console.log(me.name); // Lee
200+
console.log(me._age); // undefined
201+
202+
const you = new Person('Kim', 30);
203+
you.sayHi(); // Hi! My name is Kim. I am 30.
204+
console.log(you.name); // Kim
205+
console.log(you._age); // undefined
206+
```
207+
위 패턴을 사용하면 자바스크립트에서도 정보 은닉이 가능한 것처럼 보입니다.
208+
209+
즉시 실행 함수가 반환하는 Person 생성자 함수와 Person 생성자 함수의 인스턴스가 상속받아 호출할 Person.prototype.sayHi 메서드는 즉시 실행 함수가 종료된 이후 호출됩니다. 이때 sayHi 메서드는 즉시 실행 함수의 렉시컬 환경의 `_age`를 참조할 수 있는 클로저입니다.
210+
211+
212+
```javascript
213+
const me = new Person('Lee', 20);
214+
me.sayHi(); // Hi! My name is Lee. I am 20
215+
const you = new Person('kim', 30);
216+
you.sayHi(); // Hi! My name is Kim. I am 30
217+
218+
me.sayHi(); // Hi! My name is Lee. I am 30
219+
```
220+
하지만 지금까지 살펴본 코드에도 문제가 있습니다. Person 생성자 함수가 여러 개의 인스턴스를 생성할 경우 다음과 같이 `_age` 변수의 상태가 유지되지 않는다는 것입니다.
221+
222+
이는 `Person.prototype.sayHi` 메서드가 단 한 번 생성되는 클로저이기 때문에 발생하는 현상입니다. `Person.prototype.sayHi` 메서드는 즉시 실행 함수가 호출될 때 생성됩니다. 이때 `Person.prototype.sayHi` 메서드는 자신의 상위 스코프인 즉시 실행 함수의 렉시컬 환경의 참조를 `[Enviorment]`에 저장하여 기억합니다. (동일한 상위 스코프를 참조하게 됩니다.)
223+
224+
이처럼 자바스크립트는 정보 은닉을 완전하게 지원하지 않습니다.
225+
226+
<br>
227+
228+
## 24.6 자주 발생하는 실수
229+
```javascript
230+
var funcs = [];
231+
232+
for (var i = 0 ; i < 3 ; i++) {
233+
funcs[i] = function () { return i };
234+
}
235+
236+
for(var j = 0 ; j < funcs.length ; j++) {
237+
console.log(funcs[j]());
238+
}
239+
```
240+
1. 첫 번째 for 문의 코드 블록 내에서 함수가 funcs 배열의 요소로 추가됩니다.
241+
2. 두 번째 for 문의 코드 블록 내에서 funcs 배열의 요소로 추가된 함수를 순차적으로 호출합니다.
242+
3. 이때 funcs 배열의 요소로 추가된 3개의 함수가 0, 1, 2를 반홚할 것으로 기대하지만 결과는 그렇지 않습니다.
243+
244+
**왜 그런걸까요 ??**
245+
그 이유는 변수 선언문 var 키워드의 스코프 범위가 함수 레벨 스코프를 갖기 때문입니다. 그렇게 var로 선언된 for문 내부 변수는 전역 변수이고 전역 변수 i에는 0, 1, 2가 순차적으로 할당됩니다. 따라서 funcs 배열의 요소로 추가한 함수를 호출하면 전역 변수 i를 참조하여 i의 값 3이 출력됩니다.
246+
247+
위 문제를 해결하기 위해서는 `블록 스코프` 범위를 갖는 let 키워드를 사용하거나 for문이 순회할 때의 i 값을 기억하는 스코프를 만들어줄 수 있습니다.
248+
249+
먼저 let 키워드를 통해 위 코드를 개선해 보겠습니다.
250+
```javascript
251+
var funcs = [];
252+
253+
for (let i = 0 ; i < 3 ; i++) {
254+
funcs[i] = function () { return i };
255+
}
256+
257+
for (var j = 0 ; j < funcs.length ; j++) {
258+
console.log(funcs[j]());
259+
}
260+
```
261+
for 문의 코드 블록이 반복 실행될 때마다 for 문 코드 블록의 새로운 렉시컬 환경이 생성됩니다.
262+
이때 저장된 함수의 상위 스코프는 for 문의 코드 블록이 반복 실행될 때마다 식별자의 값을 유지해야 합니다. 이를 위해 for 문이 반복될 때마다 독립적인 렉시컬 환경을 생성하여 식별자 값을 유지합니다.
263+
264+
```javascript
265+
var funcs = [];
266+
267+
for (var i = 0 ; i < 3 ; i++) {
268+
funcs[i] = (function(id){
269+
return function() {
270+
return id
271+
}
272+
}(i))
273+
}
274+
275+
for(var j = 0 ; j < funcs.length ; j++) {
276+
console.log(funcs[j]());
277+
}
278+
```
279+
즉시 실행 함수는 전역 변수 i에 할당되어 있는 값을 인수로 전달받아 매개변수 id에 할당한 후 중첩 함수를 반환하고 종료합니다. 즉시 실행 함수가 반환한 함수는 funcs 배열에 순차적으로 저장됩니다.
280+
281+
이때 즉시 실행 함수의 매개변수 id는 즉시 실행 함수가 반환한 중첩 함수의 상위 스코프에 존재합니다. 즉시 실행 함수가 반환한 중첩 함수는 상위 스코프를 기억하는 클로저입니다.

0 commit comments

Comments
 (0)