설명에 앞서 가볍게 예시를 하나 살펴보고 가자
function() {
let a = 1;
console.log(a); // 1
}
console.log(a); // error
위의 코드를 오류없이 동작 시키려면 어떻게 해야할까?
let a = 1;
function() {
console.log(a); // 1
}
console.log(a); // 1
지역변수는 보통 function이 끝나게 되면 메모리에서 사라지기 때문에, 변수를 함수 외부로 빼 전역변수로 할당해주면 될 것이다. 그렇다면 우리는 지역변수를 계속 참조 할 수 없을까? 이를 closure가 답변해준다.
function outerFn() {
let count = 1;
function add() {
count++;
}
function output() {
console.log(count);
}
return {add, output};
}
let counter = outerFn();
counter.output(); // 1
counter.add();
counter.output(); // 2
counter.add();
counter.add();
counter.output(); // 4
위 코드에서처럼 count는 지역 변수임에도 불구하고 영속성을 갖는다. 간단히 설명하면 이게 클로져이다. JavaScript에서는 함수가 처음 선언 될 때 count가 hoisting 되고, 함수가 계속 존재하는 한 count도 지속되어진다. count는 outerFn scope에 속한다. add, output과 같은 inner scope에는 outer scope에 대한 참조가 있다. counter는 inner scope를 가리키는 변수다. counter가 지속되는 한 count또한 사라지지 않고 존재한다. 이를 다르게 말하면 특정 함수가 참조하는 변수들이 선언된 렉시컬 스코프(lexical scope)는 계속 유지되는데, 그 함수와 스코프를 묶어서 closure 라고한다.
lexical environment (어휘적 환경)
function outer() {
let a = 1;
function inner() {
console.log(a);
}
inner();
}
outer(); // 1
outer()
의 실행 결과는 1이다. outer 함수 내부에서 inner 함수를 호출하는데, inner함수에는 a가 없기 때문에 상위 스코프인 outer함수에서 a를 찾게 되는 것이다.let a = 1;
function foo() {
let a = 10;
bar();
}
function bar() {
console.log(a);
}
foo(); // 1 or 10
bar(); // 1
함수의 상위 스코프를 결정하는데에는 두 가지 방법이 있다.
따라서 위의 예시의 경우
foo()
의 실행 결과는 10foo()
의 실행 결과는 1이 나오게 된다.대부분의 프로그래밍 언어는 렉시컬 스코프이다.
function outer() {
let name = "kimcoding";
function inner() {
console.log(`hi ${name}!`); // 'hi kimcoding!'
}
inner();
return inner;
}
let greeting = outer();
greeting();
greeting()
을 실행해보면 여전히 name 이라는 변수에 접근해 hi kimcoding!
을 찍는 것을 확인할 수 있다.일반적으로 함수가 실행될 때 생성된 컨텍스트는 함수가 종료될 때 가비지컬렉션의 수집대상이 되어 사라진다. 하지만 클로져 패턴이 사용된 경우에는 내부함수의 변수가 언제 외부함수의 변수를 참조할지 알 수 없기 때문에 외부함수가 종료되어도 가비지 컬렉션의 수집대상이 되지않고 메모리상에 남아있게 된다. 이런 이유로 클로저 패턴을 남발하게 되면 메모리 누수가 발생하고 이로 인해 퍼포먼스 저하가 일어날 수 있다.