일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- BIND
- webstorm
- activation object
- This
- type
- lexical scope
- function
- Execution Context
- 함수
- hoisting
- variable object
- lexical environment
- JavaScript
- vs code
- scope chain
- 함수 표현식
- 객체
- function 표현식
- Arrow function
- 리액트 라우터
- 자바스크립트
- 화살표 함수
- react-router
- 호이스팅
- function 문
- happy hacking
- 정적스코프
- 실행컨텍스트
- moment.js
- react router
- Today
- Total
Pandaman Blog
[JS] Javascript의 클로저 (Closures) 본문
Javascript 클로저(Closures)란
클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다.
MDN에서는 위와 같이 클로저를 정의한다.
아래 예제를 통해서 클로저가 무엇인지 확인해보고 위의 정의를 이해해보자.
function handleSound() { // 1
const sound = '안녕하세요. 반갑습니다.'; // 2
function playSound() { // 3
alert(sound);
}
return playSound; // 4
}
const myFunc = handleSound(); // 5
myFunc(); // 6
위의 예제를 순서대로 살펴보자.
handleSound
함수를 선언했다.- 함수 내부에는
const
키워드의sound
라는 이름의 지역변수를 선언하고 문자열'안녕하세요. 반갑습니다.'
를 할당했다. - 함수 내부에
playSound
함수를 선언했고, 그 내부에는handleSound
함수의 지역변수sound
을 참조해 알림으로 띄우는 기능을 구현했다. playSound
함수를 리턴한다.- 전역 변수
myFunc
에 함수handleSound()
를 할당한다 - myFunc()를 실행한다.
5번 설명에서 함수 handleSound
를 실행되고 함수 playSound
를 반환하고 종료되었다. 일반적으로 함수 handleSound
실행이 끝나면 더 이상 함수 handleSound
의 지역변수sound
에 접근이 가능하지 않다고 생각한다. 하지만 함수playSound
를 참조한 myFunc
를 실행하면 handleSound
의 지역변수 name
이 참조되어 '안녕하세요. 반갑습니다.'
가 노출된다. 그 이유는 myFunc
가 참조한 함수 playSound
는 선언될 때 자신의 상위 스코프가 결정된다.(Lexical scope) 함수 playSound
의 변수 sound
는 상위 스코프에 있는 함수 handleSound
의 지역변수인 sound
를 참조하게 되어 실행이 끝나도 변수 sound
에 접근이 가능하다.
다시 MDN에서 정의한 클로저에 대해서 생각해보자. 클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다.라는 말은
함수 playSound
와 함수 playSound
가 선언되었을 때의 스코프에 있는 모든 지역변수로 구성된 어휘적 환경(Lexical Environment)의 조합이라는 말이다.
따라서 자신이 선언되었을때의 환경 밖에서 호출되어도 그 환경에 접근할 수 있게 되었다. 이처럼 클로저는 자신이 선언되었을때 환경(Lexical Environment)을 기억하는 함수라고 정의 내릴 수 있다.
클로저에 의해 참조되는 외부의 변수, 즉 handleSound
의 함수의 지역변수 sound
를 자유 변수라고 부른다.
실행컨텍스트로 클로저 이해하기
위의 예제를 실행컨텍스트로 다시 설명해보면 아래와 같다.
GlobalContext = {
LexicalEnvironment: {
EvironmentRecord: { handleSound: <function>, myFunc: undefined },
OutterEnvironment: null,
this: window
}
HandleSoundFunctionContext = {
LexicalEnvironment: {
EvironmentRecord: { playSound: <function>, sound: '안녕하세요. 반갑습니다.', arguments: null }
OutterEnvironment: <GlobalLexicalEnvironment>,
this: window
}
}
myFuncFunctionContext = { // myFunc 실행
LexicalEnvironment: {
EvironmentRecord: { arguments: null }
OutterEnvironment: <HandleSoundLexicalEnvironment>,
}
}
위에서 중요한 점은 handleSound
가 호출되고 실행 컨텍스트가 생성된다. 그리고 실행 스택에 푸시되고 함수 PlaySound
를 반환하며, 실행 스택에서 handleSound
함수실행컨텍스트는 반환된다. 이때 변수 myFunc
는 PlaySound
함수를 참조한다. myFunc
를 호출할 때 생성된 실행컨텍스트의 LexicalEnvironment
는 PlaySound
의 LexicalEnvironment
를 참조한다. 또한 myFunc
의 실행컨텍스트의 LexicalEnvironment는 PlaySound
함수가 선언되었을 때의 감싸고 있는 실행컨텍스트의 LexicalEnvironment
를 참조한다. 즉 HandleSoundFunction
실행컨텍스트의 LexicalEnvironment
를 참조하기 때문에 변수 sound
에 접근이 가능하다.
클로저는 어떤 경우에 쓰이는가?
상태 유지
클로저를 유용하게 사용할 수 있는 방법은 현재 상태를 기억하고 최신 상태로 유지하기 위해 사용될 수 있다.
아래 예제를 통해서 클로저를 사용함으로 현재 상태를 기억하고 변경된 상태를 유지되는 경우를 살펴보자.
function outterToggle() {
let freeVariable = false;
// 1. 클로저 생성 및 반환
return function closureToggle() {
// 3. 상태변경
freeVariable = !freeVariable;
alert(freeVariable);
}
};
// 2. 클로저 할당
const onToggle = outterToggle();
console.log(onToggle()); // true
console.log(onToggle()); // false
console.log(onToggle()); // true
- 함수
outterToggle
이 반환한 함수closureToggle
는 함수closureToggle
와 자신이 선언되었을 때의LexicalEnvironment
가 조합인 클로저이다. - 클로저를 변수
onToggle
에 할당했다. 클로저를onToggle
에서 제거하지 않는 클로저의LexicalEnvironment
는 소멸하지 않는다. 그 의미는 함수closureToggle
의LexicalEnvironment
가 참조한 함수outterToggle
의LexicalEnvironment
도 소멸하지 않는다 라는 의미를 포함한다. onToggle
를 호출하면onToggle
가 참조한 함수closureToggle
의 3번 내용이 실행되어 변수freeVariable
의 상태가 변경된다. 다시 말해서,onToggle
의LexicalEnvironment
가 참조한outterToggle
의LexicalEnvironment
의 변수freeVariable
의 상태가 변경된다. 따라서, 클로저는freeVariable
를 참조하기 때문에 변경된 최신 상태를 유지하게 되는 것이다.
정보의 은닉
자바와 같은 몇몇 언어들을 메소드를 프라이빗으로 선언할 수 있는 기능을 제공한다. 자바스크립트는 클로저를 이용하여 private 변수 private 메소드를 흉내 내는 것이 가능하다.
아래는 클로저를 이용해 어떻게 private 메서드, private 변수를 만들고 활용하는지에 대해서 알아보자.
const counter = function () {
let privateCount = 0;
function privateChangeCount(number) {
privateCount += number;
alert(privateCount);
}
return {
increase: function() {
privateChangeCount(1);
},
decrease: function() {
privateChangeCount(-1);
},
}
};
const counterCloser = counter();
counterCloser.increase(); // 1
counterCloser.increase(); // 2
counterCloser.decrease(); // 1
counter
의 함수 내부의 변수privateCount
와 메소드 privateChangeCount
는 함수 외부에서 직접 접근할 수 없다. 대신 반환된 함수(increase
, decrease
)를 통해서만 접근이 가능하다.
반환된 함수 increase
, decrease
는 같은 환경을 공유하는 클로저이다. '같은 환경을 공유한다'라는 것은 increase
, decrease
가 선언되었을 때의 감싸진 실행컨텍스트의 Lexical Environment
를 공유한다는 의미이다. 따라서 함수increase
, decrease
는 변수 privateCount
와 함수privateChangeCount
를 공유한다.
변수counter
를 활용해 여러 개의 카운터를 만들 수 있다.
const counterCloser1 = counter();
const counterCloser2 = counter();
counterCloser1.increase(); // 1
counterCloser2.increase(); // 1
counterCloser2.decrease(); // 0
counterCloser1.increase(); // 2
counterCloser1
와 counterCloser2
가 독립성을 유지하는 것을 확인할 수 있다. counter()
가 호출할 때 각각의 실행컨텍스트가 생성되고, 따라서, LexicalEnviroment
도 다른 버전을 갖게 된다. increase
, decrease
가 참조하는 함수가 선언되었을 때의 감싸진 실행컨텍스트의 Lexical Environment
의 버전이 다르기 때문에 서로의 영향을 받지 않게 되어 독립적으로 사용할 수 있게 된다.
문제
function count() {
var i;
for (i = 0; i < 10; ++i) {
setTimeout(function() {
console.log(i);
}, 100);
}
}
count();
위의 코드의 결과는 어떻게 될까요? 위의 코드를 실행컨텍스트 관점으로 아래와 같이 표현했다.
// 1) count의 함수실행컨텍스
countExectionCOntext = {
LexicalEnvironment: {
environmentRecord: { i: 0, arguments: null} // i는 for문을 거치면서 1씩 증가하여 최종적으로 10으로 변경됨
outterEnvironment: <GlobalLexicalEnvironment>
...
}
}
// 2) setTimeout의 함수실행컨텍스트
setTimeoutExecutionContext = {
LexicalEnvironment: {
environmentRecord: { arguments: { 0: <function>, 1: 100, length: 2 } }
outterEnvironment: <GlobalLexicalEnvironment>
...
}
}
// 3) 익명함수의 함수실행컨텍스트
AnonymousExecutionContext = {
LexicalEnvironment: {
environmentRecord: { arguments: null }
outterEnvironment: <countLexicalEnvironment>
...
}
}
count
함수가 호출되면 1번과 같이 함수컨텍스트가 생성된다.count
함수에는 변수 var 키워드의 i가 선언되어 있으며for문
이 실행된다.for문
은 총 10번 반복되고 종료된다.- for문 내부의
setTimeout
함수는 인자로 함수와 시간을 받는다. 그리고 인자로 받은 시간이 지나면 인자로 받은 함수가 실행되는 함수이다.setTimeout
함수가 실행된다. - i는 0.1초 이전 익명 함수가 실행되기 전에 1씩 증가하여 최종적으로 10으로 변경된다. 즉
count
함수의Lexical environment
의 변수i
의 값은 10이 되었다. - 0.1초가 되면 익명 함수가 호출되고 3번과 같이 실행컨텍스트가 생성된다. 10번의 loop를 거치면서 10개의 다른버전의 익명 함수의 실행컨텍스트가 생성되었다.
- 10개의 다른 버전의 익명함수 실행컨텍스트의
LexicalEnvironment
는 모두 동일한count
함수의Lexical environment
를 참조하기 때문에 값이 10으로 변경된i
을 참조하게 된다. - 따라서 변경된 i의 값, 즉
10
이 로그로 열 번 찍히는 것을 확인할 수 있다.
해결방법
일단 해결을 하기 위해서는 익명 함수가 선언될 때의 LexicalEnvironment
가 동일한 환경을 공유하지 않도록 해야 한다.
아래는 즉각 호출 함수를 이용해 for문이 실행되고 익명 함수가 선언될 때의 환경을 다르게 하는 것이다.
function count() {
var i;
for (i = 0; i < 10; ++i) {
(function outter(y) {
setTimeout(function() {
alert(y);
}, 100);
})(i);
}
}
count();
- for문이 실행되면 변수
i
가 즉각 호출 함수의 인자로 할당된다. - 할당된 인자는 다시
outter
함수의 매개변수y
로 전달된다. - 전달된
y
는 그대로 익명 함수의 인수로 전달된다.
여기서 주목해야 할 점은 익명 함수가 선언될 때 감싸고 있는 실행컨텍스트는otter
함수의 실행 컨텍스트인데, 익명 함수의 실행컨텍스트의LexicalEnvironment
는 익명 함수가 선언될 때 감싸고 있는 실행컨텍스트의LexicalEnvironment
를 참조한다.
for문에 의해 반복될 때마다 함수outter
는 다른 버전의 실행컨텍스트를 생성하게 된다. 그러므로outter
의 실행컨텍스트의LexicalEnvironment
도 다른 버전을 갖게 되고y
역시 달라지게 된다. 따라서, 반복문마다 익명 함수가 선언될 때 참조하는 환경이 다르게 되며, 결론적으로 익명 함수에서 참조하는 변수y
도 다르게 되어 우리가 원하는 값을 얻을 수 있다.
다음은 블록 스코프를 이용하여 위 문제를 해결할 수 있다.
function count() {
for (let i = 0; i < 10; ++i) {
setTimeout(function() {
console.log(i);
}, 100);
}
}
count();
for문이 반복될 때마다 다른 버전의 LexicalEnvironment
를 생성하게 된다. for (let i...
에서 선언한 변수 역시, 해당 LexicalEnvironment
환경에 국한된다.
따라서 반복문마다 생성된 익명 함수의 실행컨텍스트의 LexicalEnvironment
는 익명 함수가 선언될 때 블록의 LexicalEnvironment
를 참조를 하게 되어 우리가 원하는 i
을 얻을 수 있다.
위 코드를 for문의 스코프를 쉽게 풀이한다면 아래와 같이 표현할 수 있다.
{
let i = 0;
setTimeout(function() {
console.log(i);
}, 100);
}
{
let i = 1;
setTimeout(function() {
console.log(i);
}, 100);
}
...
{
let i = n - 1;
setTimeout(function() {
console.log(i);
}, 100);
}
블록에서 익명 함수의 Lexical Environment
는 각각 다르다는 것을 한눈에 확인할 수 있다.
마치며
자바스크립트는 참 신기하면서도 재밌는 언어이다. 하지만 자바스크립트 개발자라면 신기하게만 바라보면 안 된다. 왜 이러한 현상들이 일어나는지 철저히 파악해야 한다!!
클로저를 개념으로만 알고 있었지만, 실행컨텍스트를 통해서 분석하고, 이해하니 속이 다 후련하다. 여러분들도 깊게 이해하고 싶다면 필자처럼 분석하는 것도 좋은 방법이라고 생각한다.
여러분들의 Feedback은 적극 환영합니다. 감사합니다.
파트너스 활동을 통해 일정액의 수수료를 제공받을 수 있음
'Front end > Javascript' 카테고리의 다른 글
[JS] Javascript의 Event (0) | 2020.02.28 |
---|---|
[JS] Javascript의 Event Loop (이벤트 루프) (3) | 2020.02.22 |
[JS] Javascript의 실행 컨텍스트 (Execution Context) (3) | 2020.02.06 |
[JS] Javascript의 Scope (스코프) (2) | 2020.02.02 |
[JS] Javascript의 Prototype (프로토타입) (6) | 2020.01.21 |