MDN에서는 클로저를 함수와 함수가 선언된 렉시컬 환경의 조합으로 정의한다.
외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다. 이러한 중첩 함수를 클로저closure라고 부른다.
자바스크립트는 렉시컬 스코프를 따른다. 렉시컬 환경의 “외부 렉시컬 환경에 대한 참조”에 저장할 참조값, 즉 상위 스코프에 대한 참조는 함수 정의가 평가되는 시점에 함수가 정의된 환경에 의해 결정된다.
const x = 1;
function outer() {
const x = 10;
const inner = function () { console.log(x); };
return inner;
}
const innerFunc = outer();
innerFunc(); // 10
위 예제에서 inner
함수는 자신이 평가될 때 자신이 정의된 위치에 의해 결정된 상위 스코프, 즉 outer
함수의 렉시컬 환경을 함수 객체의 내부 슬롯 [[Environment]]
에 저장한다. 이때 저장된 상위 스코프는 함수가 존재하는 한 유지된다.
outer
함수를 호출하면 outer
함수는 중첩 함수 inner
를 반환하고 생명 주기를 마감한다. 즉, outer
함수의 실행이 종료되면 outer
함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거된다. 이때 outer
함수의 실행 컨텍스트는 실행 컨텍스트 스택에서 제거되지만 outer
함수의 렉시컬 환경까지 소멸하는 것은 아니다.
outer
함수의 렉시컬 환경은 inner
함수의 [[Environment]]
내부 슬롯에 의해 참조되고 있고 inner
함수는 전역 변수 innerFunc
에 의해 참조되고 있으므로 가비지 컬렉션의 대상이 되지 않기 때문이다. 가비지 컬렉터는 누군가가 참조하고 있는 메모리 공간을 함부로 해제하지 않는다.
outer
함수가 반환한 inner
함수를 호출하면 inner
함수의 실행 컨텍스트가 생성되고 실행 컨텍스트 스택에 푸시된다. 그리고 렉시컬 환경의 외부 렉시컬 환경에 대한 참조에는 inner
함수 객체의 [[Environment]]
내부 슬롯에 저장되어 있는 참조값이 할당된다.
중첩 함수 inner
는 외부 함수 outer
보다 더 오래 생존했다. 이때, 외부 함수보다 더 오래 생존하는 중첩 함수는 외부 함수의 생존 여부와 상관없이 자신이 정의된 위치에 의해 결정된 상위 스코프를 기억한다. 상위 스코프를 참조할 수 있으므로 상위 스코프의 식별자를 참조할 수 있고 식별자의 값을 변경할 수도 있다.
inner
함수와 같이 (1) 중첩 함수가 상위 스코프의 식별자를 참조하고 있고 (2) 중첩 함수가 외부 함수보다 더 오래 유지되는 경우 그 중첩 함수를 클로저라고 한다.
클로저는 상태가 의도치 않게 변경되지 않도록 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용하여, 상태를 안전하게 변경하고 유지하기 위해 사용한다.
const counter = (function () {
let num = 0;
return {
increase() {
return ++num;
},
decrease() {
return --num;
}
};
}());
console.log(counter.increase()); // 1
console.log(counter.increase()); // 2
console.log(counter.decrease()); // 1
console.log(counter.decrease()); // 0
위 예제에서 increase
, decrease
함수는 즉시 실행 함수(IIFE)의 식별자(num
)를 참조하는 클로저다. num
상태는 increase
, decrease
함수로만 변경할 수 있고 외부에서 직접 접근하거나 변경할 수 없는 private 변수이므로, 의도되지 않은 변경을 걱정할 필요 없이 안전하게 유지된다.
var
사용 시 주의아래 콘솔 출력 결과는 어떻게 될까?
let funcArr = [];
for (var i = 0; i < 5; i++) {
var num = i * 2;
funcArr.push(() => console.log(num));
}
funcArr.forEach(fn => fn());
var
키워드로 선언한 변수는 블록 레벨 스코프가 아닌 함수 레벨 스코프를 따르기 때문에 여기서 for문은 지역 스코프로 인정되지 않는다. 즉, var
키워드로 선언한 num
은 전역 변수이므로 num
에는 for문 반복이 끝난 값인 8이 할당되어 8 8 8 8 8
이 출력된다.
클로저를 이용해서 0 2 4 6 8
이 출력되도록 바꿔보자.
let funcArr = [];
for (var i = 0; i < 5; i++) {
(function() {
var num = i * 2;
funcArr.push(() => console.log(num));
})();
}
funcArr.forEach(fn => fn()); // 0 2 4 6 8
즉시 실행 함수(IIFE)로 감싸주면 var
키워드로 선언한 num
은 함수 레벨 스코프에 따라 즉시 실행 함수의 지역 변수가 된다. funcArr
에 푸시되는 함수는 즉시 실행 함수 내부의 중첩 함수로서, 즉시 실행 함수를 "외부 렉시컬 환경에 대한 참조"에 저장하는 클로저다. 따라서 반복문 루프가 돌 때마다 각각의 렉시컬 환경에 등록되는 num
변수를 참조하므로 0 2 4 6 8
이 출력된다.
setTimeout
함수와 var
vs let
function fn(){
for (var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 0);
}
}
fn(); // 10 10 10 10 10 10 10 10 10 10
setTimeout
함수가 호출되면 setTimeout
함수의 실행 컨텍스트가 생성되고 콜 스택에 푸시되어 현재 실행 중인 실행 컨텍스트가 된다. 타이머가 만료되면 setTimeout
함수의 콜백 함수는 태스크 큐에 푸시되어 대기하다가 콜 스택이 비었을 때, 즉 전역 코드 실행이 종료되었을 때 호출된다.
이때 setTimeout
함수의 콜백 함수는 fn
함수 스코프를 "외부 렉시컬 환경에 대한 참조"에 저장하는 클로저다. 즉 i
변수는 var
키워드로 선언되어 fn
함수의 렉시컬 환경에 등록된 i
를 참조하는데, 이 i
변수에는 반복문이 종료되고 10
이 할당되므로 10
이 10번 출력된다.
0부터 9까지 출력하려면 var
키워드를 let
키워드로 바꾸면 된다.
function fn(){
for (let i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 0);
}
}
fn(); // 0 1 2 3 4 5 6 7 8 9
마찬가지로 setTimeout
함수의 콜백 함수는 fn
함수의 중첩 함수로서 i
를 참조하는 클로저인데, 이번에 i
변수는 let
키워드로 선언되었으므로 블록 레벨 스코프를 따른다. 따라서 반복문 루프마다 독립적인 렉시컬 환경이 생성되어 각각의 i
값을 참조하므로 0부터 9까지 출력된다.
클로저를 이용해서 React의 useState
훅을 비슷하게(실제로는 더 복잡하지만 여기서는 간단하게) 구현할 수 있다.
useState
훅의 주요 특징은 클로저의 주요 특징과 유사하다.
state
값을 기억한다.setState
이외 다른 함수로는 state
값을 변경할 수 없다. (정보 은닉)const MyReact = (function() {
let state;
return {
useState(initialValue) {
if (state === undefined) state = initialValue;
function setState(newValue) {
state = newValue;
}
return [state, setState];
}
};
})();
function Counter() {
const [count, setCount] = MyReact.useState(0);
return {
click() {
setCount(count + 1);
},
render() {
return `count: ${count}`;
}
}
}
Counter().render(); // 'count: 0'
Counter().click();
Counter().render(); // 'count: 1'
Counter().click();
Counter().render(); // 'count: 2'
Counter().click();
Counter().render(); // 'count: 3'
state
값을 기억하기 위해 외부 함수인 MyReact
에서 state
를 선언한다. useState
함수는 즉시 실행 함수 MyReact
의 자유 변수인 state
값을 참조하는 클로저이므로, MyReact
실행 컨텍스트가 종료되어도 state
값은 사라지지 않고 계속 기억할 수 있게 된다.setState
함수 또한 즉시 실행 함수 MyReact
의 자유 변수인 state
값을 참조하는 클로저이다. state
변수는 오로지 setState
함수에 의해서만 변경될 수 있으므로 외부에서의 변경으로부터 안전하다.참고
[JS]클로져(closure)와 클로져의 사용 예제
Part 12: setTimeout + Closures Interview Questions 🤓
React 의 useState와 closure