앞 포스트에서 다뤘던 Hoisting과 함께 JavaScript 필수 이론격으로 언급되는 Closure에 대해서 다뤄보자🏃♂️
(뭐라..고요..?)함수와 함수가 선언된 어휘적 환경(Lexical environment)의 조합이다.
출처 MDN
간단하게 말하자면 함수 밖에서 선언된 변수를 함수 내부에서 사용할 때 Closure가 생겨난다고 할 수 있다.
사실 Closure는 JavaScript 고유의 개념은 아니고 함수형 프로그래밍 언어에서 사용되는 중요한 특성이다.
또한 Closure를 이해하려면 Execution Context (실행 컨텍스트)에 대해 알고 있어야 하는데 자세한 내용은 따로 다루기로 하고 간단하게 말하면 다음과 같다.
⚡ Execution Context
- 실행 가능한 코드를 형상화하고 구분하는 추상적인 개념
- 실행 가능한 코드가 실행되기 위해 필요한 환경
- 실행 할 코드에 제공할 환경 정보들을 모아놓은 객체
예제 코드와 함께 Closure에 대해서 정리해보자
(이 코드는 Closure가 아니다!)
function outerFunc() {
var name = "minkyoung";
var innerFunc = function() { console.log(name);};
innerFunc();
}
outerFunc(); // minkyoung
외부함수 outerFunc
안에 내부함수 innerFunc
이 선언되고 호출까지 되고 있는 것을 확인 할 수 있다. 이때 innerFunc
은 자신을 포함하고 있는 외부함수 outerFunc
의 변수인 name
에 접근이 가능하다.
이게 가능한 이유는 Lexical Scoping 때문인데 Scope는 함수를 호출할 때가 아니라 함수를 어디에 선언했는지에 따라서 결정된다는 뜻이다.
내부함수 innerFunc
이 호출되면 자신의 실행 컨텍스트가 실행 컨텍스트 스택에 쌓이게 되고, Variable Object(변수 객체)와 Scope Chain(스코프 체인), 그리고 this에 바인딩 할 객체가 결정된다.
이때 Scope Chain은 Global Scope를 가리키는 전역 객체와 함수 outerFunc
의 Scope를 가리키는 함수 outerFunc
의 활성 객체 그리고 함수 자신의 Scope를 가리키는 활성 객체를 순차적으로 바인딩한다. Scope Chain이 바인딩한 객체가 바로 Lexical Scope의 실체이다.
내부함수 innerFunc
이 실행될 때 자신을 포함하고 있는 외부함수 outerFunc
의 변수 name
에 접근 할 수 있는 것, 다시말해 상위 스코프에 접근 할 수 있는 것은 Lexical Scope의 레퍼런스를 차례대로 저장하고 있는 실행 컨텍스트의 Scope Chain을 JavaScript 엔진이 검색했기 때문에 가능했던 것이다.
innerFunc
함수 Scope내에서 변수 name
을 검색하지만 찾지 못한다.innerFunc
함수를 포함하는 외부함수 outerFunc
함수 Scope에서 변수 name
을 검색, 성공한다.위의 예제 코드를 조금 바꿔서 내부함수 innerFunc
을 외부함수 outerFunc
내에서 호출하는 것이 아니라 반환하도록 해보자
Closure 코드
function outerFunc() {
var name = "minkyoung";
var innerFunc = function() { console.log(name);};
return innerFunc;
}
/**
* 함수 outerFunc을 호출하면 내부함수 innerFunc이 반환된다
* 함수 outerFunc의 실행 컨텍스트는 소멸된다.
*/
var inner = outerFunc();
inner(); // minkyoung
이 코드에선 함수 outerFunc
은 내부함수 innerFunc
을 return 하고 소멸되었다. 즉 outerFunc
는 실행된 이후 실행 컨텍스트 stack에서 제거되었으므로 함수 outerFunc
의 변수 name
또한 유효하지 않게 되어 오류가 뜰 것 같지만 예상과는 반대로 정상적으로 출력이 되는 것을 확인 할 수 있다.
이처럼 자신을 포함하고 있는 외부함수보다 내부함수가 더 오래 유지되는 경우, 외부함수 밖에서 내부함수가 호출되더라도 외부함수의 지역 변수에 접근할 수 있는데 이러한 함수를 Closure 라고 부른다.
함수와 함수가 선언된 어휘적 환경(Lexical environment)의 조합이다.
이제 위에서 이해하지 못했던 이 말을 다시 본다면, 함수 = 내부함수
를 의미하고 함수가 선언된 어휘적 환경 = 내부함수가 선언됐을 때의 Scope
를 의미하는 것이다.
Closure는 return 된 내부함수가 자신이 선언됐을 때의 Lexical Enviroment인 Scope를 기억하여 자신(내부함수)이 선언됐을 때의 Scope 밖에서 호출되어도 그 Scope에 접근할 수 있는 함수를 의미한다.
다시말해 자신이 생성되었을 때의 환경을 기억하는 함수라고 할 수 있다.
Closure가 가장 잘 사용되는 상황은 현재 상태를 기억하고, 변경된 최신 상태를 유지하는 상황이다.
<!DOCTYPE html>
<html>
<body>
<button class="toggle">+</button>
<div class="box" style="width: 100px; height: 100px; background: black;"></div>
<script>
var box = document.querySelector('.box');
var btn = documnet.querySelector('.toggle');
var toggle = (function() {
var isOpen = false;
return function() { // 1. Closure를 return
box.style.display = isOpen ? 'block' : 'none';
isOpen = !isOpen; // 3. 상태 변경
};
})();
btn.onClick = toggle; // 2. 이벤트 프로퍼티에 Closure 할당
</script>
</body>
</html>
위의 예제는 +
라는 토글 버튼을 누르면 div
가 보였다가 안보였다가 하는 코드이다.
isOpen
을 기억한다.isOpen
은 소멸하지 않는다.isOpen
의 값이 변경된다. isOpen
은 Closure에 의해 참조되고 있기 때문에 유효하며, 최신 상태를 계속해서 유지한다.Closure의 경우에는 정말 많은 자료를 찾아봤지만 실행 컨텍스트에 대해서 사전에 알고 있어야 해서 이해하기가 정말 정말 정말 어려웠다..
찾다가 설명을 쉽게 해주신 분의 글을 찾아서 한번 쭉 읽고 나서 따라적고 내 머리로 이해 가능한 말로 조금씩 바꾸면서 이해하려고 노력했다 😥
실행 컨텍스트에 대해서 정리할 때 Closure를 한번 더 보고 마스터 해야겠다...