MDN Web Docs에 따르면, 클로저(closure)란 함수와 함수가 선언된 어휘적 환경의 조합 이다.
더 쉽게 말하면, 클로저는 외부 함수 호출이 종료되더라도 외부 함수의 지역 변수를 내부 함수가 사용할 수 있는 구조 라고 할 수 있겠다.
클로저에 대해 설명하기 전에, 아래 내용을 먼저 이해하고 넘어가자.
아래 코드를 실행하면 어떤 일이 일어날까?
function init() {
function displayName() {
var name = "Look at me!";
alert(name);
}
displayName();
}
init();
이 경우, init()
은 "Look at me!"
라는 메시지를 alert한다.
자바스크립트 파서는 함수가 선언될 때 해당 함수의 스코프 를 결정한다. 이걸 Lexical scoping 이라고 한다.
자바스크립트 엔진은alert(name)
을 실행하기 위해 displayName()
의 스코프를 탐색해 name = "Look at me!"
라는 값을 찾아내고, alert
로 출력한다.
그럼 아래와 같은 경우는 어떨까?
var x = 1;
function first() {
var x = 10;
second();
}
function second() {
console.log(x);
}
first();
second();
10
과 1
이 출력될 것이라 생각할 수 있지만, 사실 위의 코드는 1
과 1
을 출력한다. 이는 second()
가 first()
안에서 호출되었지만, 그와 상관없이 second()
는 global
범위에 선언되어 있으므로 global
의 x값인 1을 출력한 것이다.
클로저가 기억하는 '환경' 이란, 바로 Lexical scope를 말한다.
그렇다면 아래 코드를 실행하면 어떤 일이 일어날까?
function init() {
var name = "Mozilla";
function displayName() { // displayName() 은 내부 함수이며, 클로저다.
alert(name); // 부모 함수에서 선언된 변수를 사용한다.
}
displayName();
}
init();
이 경우, init()
은 "Mozilla"
라는 메시지를 alert한다. 왜 위 코드와 다른 결과가 나왔을까?
자바스크립트 엔진은alert(name)
을 실행하기 위해 displayName()
의 스코프를 탐색한다. 하지만 name
을 찾을 수가 없다!
이런 경우, 자바스크립트는 스코프 체인 을 따라 상위 스코프에서 변수를 찾는다.
아래 코드에서 일어나는 일을 생각해 보자.
var color = 'red';
function foo() {
var color = 'blue'; // 2
function bar() {
console.log(color); // 1
}
return bar;
}
var baz = foo(); // 3
baz(); // 4
bar
는 color
를 console
출력하는 함수로 선언되었다.bar
의 스코프가 결정된다.global
에서 baz(=bar)
를 호출했다.bar
는 미리 결정된 자신의 스코프에서 color
를 찾는다.foo
의 스코프를 뒤져 color='blue'
를 찾는다. 위 코드에서 baz(=bar)
는 클로저이기 때문에 global
에서 호출되었음에도 불구하고 호출된 위치와 관련 없이 foo
에서 color
를 탐색한다.
아래와 같은 코드에서는 어떤 일이 벌어질까?
function count() {
var i;
for (i = 1; i < 10; i += 1) {
setTimeout(function timer() {
console.log(i);
}, i*100);
}
}
count();
놀랍게도 1,2,3,4,...9가 0.1초마다 출력되는 대신, 10이 9번 출력된다. 앞선 예제와 같이 코드에서 벌어지는 일을 해석해 보자.
timer
는 i
를 출력하는 함수로 선언되었다.timer
의 스코프가 결정된다.setTimeout
으로 인한 0.1초의 대기시간이 지날 동안, for
문이 종료되고 i = 10
이 된다. timer
가 호출된다.timer
는 자신의 스코프에서 i
를 찾는다.foo
의 스코프를 뒤져 i=10
을 찾는다. time
이 호출된다. 위의 과정을 반복하여 i=10
을 찾는다. 그럼 의도대로 1~9를 출력하고 싶다면 어떤 방법을 사용해야 할까?
첫 번째로, 새로운 스코프를 추가하여 반복 시마다 각각 따로 값을 저장할 수 있다.
function count() {
var i;
for (i = 1; i < 10; i += 1) {
(function(countingNumber) {
setTimeout(function timer() {
console.log(countingNumber);
}, i * 100);
})(i);
}
}
count();
위 코드에서, timer
는 즉시 실행 함수(IIFE)의 스코프에서 countingNumber
를 찾는다. timer
는 IIFE의 스코프에서 i
의 값이 저장된 countingNumber
를 찾아 출력한다.
두 번째로, var 대신 let을 사용할 수 있다.
function count() {
'use strict';
for (let i = 1; i < 10; i += 1) {
setTimeout(function timer() {
console.log(i);
}, i * 100);
}
}
count();
위 코드에서는 var 대신 let이 사용되었다. var는 함수 레벨 스코프를 따르지만, let은 블록 레벨 스코프를 따른다. var를 사용했을 때, i
의 스코프는 count()
함수 내부가 된다. 그러나 let을 사용했을 때, i
의 스코프는 for
문 내부가 된다.
달리 말해, for
문이 돌면서 i=0
인 스코프, i=1
인 스코프, i=2
인 스코프...를 생성하고, timer()
가 실행될 시 각각 해당하는 스코프에서 i
를 찾아 출력하기에 1~9가 출력된다.
각각의 클로저는 환경을 기억한다. 그 말은 메모리가 소모된다는 뜻이다. C++에서 동적 할당으로 생성한 객체를 delete하듯이, 사용이 끝난 클로저는 아래와 같이 참조를 제거해야 한다.
function myName(name) {
var _name = name;
return function() {
console.log('my name is ' + _name);
};
}
var myName1 = myName('철수');
var myName2 = myName('영희');
myName1(); // 'my name is 철수'
myName2(); // 'my name is 영희'
// 메모리 release
myName1 = null;
myName2 = null;
참고: