“A closure is the combination of a function and the lexical environment within which that function was declared.”
클로저는 함수와 그 함수가 선언됐을 때의 렉시컬 환경(Lexical environment)과의 조합이다.
코어 자바스크립트에서 정의한 클로저에 대해 좀 더 알아보면
어떤 함수 A에서 선언한 변수 a를 참조하는 내부 함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상
그동안 여러 JS강의에서 들었던 클로저에 대한 설명을 종합해보자면,
함수 중첩 시 자식 함수가 부모 함수 범위에 접근 가능한 것, 즉 부모 함수 안에서 자식 함수를 선언하면 자식함수를 어디에서 호출하더라도 자식함수 안에서 부모함수의 변수에 접근할 수 있다고 설명한다.
함수형 프로그래밍 언어에서 사용되는 특성 중 하나이므로 ECMA Script에는 정의되어 있지 않다.
클로저에 대해 좀 더 명확하게 이해하기 위해서는 실행 컨텍스트에 대해 이해하는 것이 필요하다.
function outerFunc() {
var x = 10;
var innerFunc = function () {
console.log(x);
};
innerFunc();
}
outerFunc(); // 10
내부함수 innerFunc
가 호출되면 자신의 실행 컨텍스트가 실행 컨텍스트 스택에 쌓이고 변수 객체(Variable Object)와 스코프 체인(Scope chain) 그리고 this에 바인딩(실제 값 또는 프로퍼티를 확정)할 객체가 결정된다. 이때 스코프 체인은 전역 스코프를 가리키는 전역 객체와 함수 outerFunc
의 스코프를 가리키는 함수 outerFunc
의 활성 객체(Activation object) 그리고 함수 자신의 스코프를 가리키는 활성 객체를 순차적으로 바인딩한다. 스코프 체인이 바인딩한 객체가 바로 렉시컬 스코프의 실체이다.
내부함수 innerFunc가 자신을 포함하고 있는 외부함수 outerFunc의 변수 x에 접근할 수 있는 것, 다시 말해 상위 스코프에 접근할 수 있는 것은 렉시컬 스코프의 레퍼런스를 차례대로 저장하고 있는 실행 컨텍스트의 스코프 체인을 자바스크립트 엔진이 검색하였기에 가능한 것이다.
스코프는 함수의 유효범위를 함수를 어디에서 실행했느냐가 아니라 어디서 선언(정의)되었느냐에 따라 결정된다. 함수는 동적이지만 함수 정의는 정적이기 때문이다. 이를 렉시컬 스코핑(Lexical scoping)라 한다.
정적 스코프 = 렉시컬 스코프
위 예제의 함수 innerFunc는 함수 outerFunc의 내부에서 선언되었기 때문에 함수 innerFunc의 상위 스코프는 함수 outerFunc이다. 함수 innerFunc가 전역에 선언되었다면 함수 innerFunc의 상위 스코프는 전역 스코프가 된다.
function makeAdder(x){
return function(y) { // y를 가지고 있고 상위함수인 makeAdder의 x에 접근 가능
return x + y ;
}
}
const add3 = makeAdder(3);
console.log(add3(2)) // 5.
// add3 함수가 생성된 이후에도 상위함수인 makeAdder의 x에 접근 가능
const add10 = makeAdder(10);
console.log(add10(5)); // 15
console.log(add3(1)); // 4
=> 즉, 반환된 내부함수가 자신이 선언되었을 때의 렉시컬 스코프를 기억하여 자신이 선언되었을 때의 스코프 밖에서 호출이 되어도 그 환경에 접근할 수 있는 함수
직접적으로 변경하면 안되는 변수에 대해 접근을 막는 것을 은닉화(객체에서 속성을 직접 접근하지 못하게 숨기는 것)라고 한다.
function a(){
let temp = 'a'
return temp;
}
// console.log(temp) error: temp is not defined
const result = a()
console.log(result); //a
현재 위 함수 내부적으로 선언된 temp에는 직접적으로 접근을 할 수 없다. 함수 a를 실행시켜 그 값을 result라는 변수에 담아 클로저를 생성함으로써 temp의 값에 접근이 가능하다.
function Hello(name) {
this._name = name;
}
Hello.prototype.say = function() {
console.log('Hello, ' + this._name);
}
let a = new Hello('영서');
let b = new Hello('아름');
a.say() //Hello, 영서
b.say() //Hello, 아름
a._name = 'anonymous'
a.say() // Hello, anonymous
function hello(name) {
let _name = name;
return function () {
console.log('Hello, ' + _name);
};
}
let a = new hello('영서');
let b = new hello('아름');
a() //Hello, 영서
b() //Hello, 아름
<script>
var incleaseBtn = document.getElementById('inclease');
var count = document.getElementById('count');
// 카운트 상태를 유지하기 위한 전역 변수
var counter = 0;
function increase() {
return ++counter;
}
incleaseBtn.onclick = function () {
count.innerHTML = increase();
};
</script>
<script>
var incleaseBtn = document.getElementById('inclease');
var count = document.getElementById('count');
function increase() {
// 카운트 상태를 유지하기 위한 지역 변수
var counter = 0;
return ++counter;
}
incleaseBtn.onclick = function () {
count.innerHTML = increase();
};
</script>
<script>
var incleaseBtn = document.getElementById('inclease');
var count = document.getElementById('count');
var increase = (function () {
// 카운트 상태를 유지하기 위한 자유 변수
var counter = 0;
// 클로저를 반환
return function () {
return ++counter;
};
}());
incleaseBtn.onclick = function () {
count.innerHTML = increase();
};
</script>
useState는 setState를 통해 상태를 변경하고, 함수가 실행되었을 때 이전 상태를 기반으로 상태가 변경되며 항상 최신 상태를 유지한다.
실제로 컴포넌트에서 상태의 변경을 감지하기 위해서 함수가 실행되었을 때 이전 상태에 대한 정보를 가지고 있어야 한다. 그리고 이 과정에서 클로저를 사용한다.
function useState(initialState) {
var dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
useState의 내부 코드는 위와 같이 정의되어있다.
dispatcher에는 resolveDispatcher함수의 결과가 할당되고, dispatcher의 useState메서드에 초기값을 전달하고 반환된 값을 리턴한다.
function resolveDispatcher() {
var dispatcher = ReactCurrentDispatcher.current;
return dispatcher;
}
그리고 resolveDispatcher는 ReactCurrentDispatcher라는 객체의 current프로퍼티를 반환하는데
여기서 ReactCurrentDispatcher는 전역에 설정되어있는 객체이다.
따라서 useState가 반환한 상태의 배열은 전역객체로부터 오고 hook을 호출하면 클로저 동작에 의해 컴포넌트 밖에 있는 외부 스코프에 존재하는 state값에 접근, 참조할 수가 있다. 컴포넌트에서 값의 변경이 있으면 외부의 값이 변경되고 컴포넌트 내부에서 업데이트된다.