클로저 Closure
: 여러 함수형 프로그래밍 언어에서 등장하는 보편적인 특성
lexical environment의 상호관계에 따른 현상LexicalEnvironment의 environmentRecord와 outerEnvironmentReference에 의해 변수의 유효범위인 스코프가 결정되고 스코프 체인이 가능해짐outerEnvironmentReference가 참조하는 대상인 A의 LexicalEnvironment에도 접근이 가능lexical environment의 상호관계 가 유의미하다.var outer = function () {
var a = 1;
var inner = function () {
console.log(++a); // 2
};
inner();
};
outer();
inner 함수 내부에서 a를 선언하지 않았기 때문에
environmentRecord에서 값을 찾지 못하므로
outerEnvironmentReference에 지정된 상위 컨텍스트인
outer의 LexicalEnvironment에 접근해서 다시 a를 찾는다.
outer 함수의 실행 컨텍스트가 종료되면
LexicalEnvironment에 저장된 식별자들 (a, inner)에 대한 참조를 지운다.
그러면 각 주소에 저장되어 있던 값들은 자신을 참조하는 변수가 하나도 없게 되므로
가비지 컬렉터의 수집 대상이 된다.

var outer = function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner();
};
var outer2 = outer();
console.log(outer2); // 2
inner 함수를 실행한 결과를 리턴하므로
outer 함수의 실행 컨텍스트가 종료된 시점에는 a변수를 참조하는 대상 없어진다.
a, inner 변수 값들은 언젠가 가비지 컬렉터에 의해 소멸될 것이다.
(1), (2) 모두 outer 함수의 실행 컨텍스트가 종료되기 이전에
inner 함수의 실행 컨텍스트가 종료돼 있으며,
이후 별도로 inner 함수를 호출할 수 없다.
-> outer 실행 컨텍스트 종료 후에도 inner 함수 호출할 수 있게 만들면?
var outer = function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner;
};
var outer2 = outer();
console.log(outer2()); // 2
console.log(outer2()); // 3
inner 함수의 실행 결과가 아닌 inner 함수 자체를 반환하고 있다.
outer 함수의 실행 컨텍스트가 종료될 때 outer2 변수는 outer의 실행 결과인 inner 함수를 참조하게 된다.
outer2를 호출하면 앞서 반환된 함수인 inner가 실행된다.
inner 함수의 실행 컨텍스트의 environmentRecord에는 수집할 정보가 없다.
outerEnvironmentReference에는 inner 함수가 선언된 위치의 LexicalEnvironment가 참조복사된다.
inner 함수는 outer 함수 내부에서 선언됐으므로, outer 함수의 LexicalEnvironment가 담긴다.
스코프 체이닝에 따라 outer에서 선언한 변수 a에 접근해서 1만큼 증가시킨 후 그 값인 2를 반환하고,
inner 함수의 실행 컨텍스트가 종료된다.
outer2 호출하면 같은 방식으로 a의 값을 2에서 3으로 1증가 시킨 후 3을 반환한다.
inner 함수의 실행 시점에는 outer 함수는 이미 실행이 종료된 상태인데
outer 함수의 LexicalEnvironment에 어떻게 접근할 수 있을까?
=> 가비지 컬렉터의 동작 방식 때문
가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않는다.
outer 함수는 실행 종료 시점에 inner 함수를 반환한다.
외부 함수인 outer의 실행이 종료되더라도 내부 함수인 inner 함수는 언젠가 outer2를 실행함으로써 호출될 가능성이 열린다.
언젠가 inner 함수의 실행 컨텍스트가 활성화되면 outerEnvironment가 outer 함수의 LexicalEnvironment를 필요로 할 것이므로 수집대상에서 제외된다.
그 덕에 inner 함수가 이 변수에 접근할 수 있다.

클로저: 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우
A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는현상
(1), (2)에서는 outer의 LexicalEnvrionment에 속하는 변수가 모두 가비지 컬렉팅 대상에 포함된 반면,
(3)에서는 변수 a가 대상에서 제외됐다.
함수의 실행 컨텍스트가 종료된 후에도
LexicalEnvironment가 가비지 컬렉터의 수집 대상에서 제외되는 경우
= 지역변수를 참조하는 내부함수가 외부로 전달된 경우가 유일하다.
주의할 점 외부로 전달 이 return만을 의미하는 것은 아니다.
두 상황 모두 지역변수를 참조하는 내부함수를 외부에 전달하기 때문에 클로저이다.
외부객체인 window의 메서드 setInterval/setTimeout에 전달할 콜백 함수 내부에서 지역변수를 참조한다.
(function () {
var a = 0;
var intervalId = null;
var inner = function () {
if (++a >= 10) {
clearInterval(intervalId);
}
console.log(a);
};
intervalId = setInterval(inner, 1000);
})();
별도의 외부객체인 DOM의 메서드 addEventListener에 등록할 handler 함수 내부에서 지역변수를 참조한다.
(function () {
var count = 0;
var button = document.createElement('button');
button.innerText = 'click';
button.addEventListener('click', function () {
console.log(++count, 'times clicked');
});
document.body.appendChild(button);
})();
클로저 특성
메모리 소모 관리법
참조 카운트를 0으로 만드는 방법
return에 의한 클로저의 메모리 해제
var outer = function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner;
};
var outer2 = outer();
console.log(outer2()); // 2
console.log(outer2()); // 3
outer = null; // outer 식별자의 inner 함수 참조를 끊음
setInterval에 의한 클로저의 메모리 해제
(function () {
var a = 0;
var intervalId = null;
var inner = function () {
if (++a >= 10) {
clearInterval(intervalId);
inner = null; // inner 식별자의 함수 참조를 끊음
}
console.log(a);
};
intervalId = setInterval(inner, 1000);
})();
eventListener에 의한 클로저의 메모리 해제
(function () {
var count = 0;
var button = document.createElement('button');
button.innerText = 'click';
var clickHandler = function () {
console.log(++count, 'times clicked');
if (count >= 10) {
button.removeEventListener('click', clickHandler);
clickHandler = null; // clickHandler 식별자의 함수 참조를 끊음
}
};
button.addEventListener('click', clickHandler);
document.body.appendChild(button);
})();
콜백 함수(이벤트 리스너)와 클로저
콜백 함수를 내부 함수로 선언해서 외부변수를 직접 참조하는 방법 (1)
var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');
fruites.forEach(function (fruit) { // A
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', function () { // B
alert('yout choice is ' + fruit);
});
$ul.appendChild($li);
});
document.body.appendChild($ul);
fruits 변수 순회하며 li 생성한다.
li 클릭하면 해당 리스너에 기억된 콜백 함수 실행된다.
addEventListener에 넘겨준 콜백 함수B에는 fruit라는 외부 변수를 참조하고 있으므로 클로저 있다.
A는 fruits 개수만큼 실행되며, 그때마다 새로운 실행 컨텍스트 활성화된다.
A의 실행 종료 여부와 무관하게 클릭 이벤트에 의해 각 컨텍스트의 B가 실행될 때 B의 outerEnvironmnetReference가 A의 LexicalEnvironment를 참조하게 된다.
최소한 B 함수가 참조할 예정인 변수 fruit에 대해서는 A가 종료된 후에도 GC 대상에서 제외되어 계속 참조 가능하다.
B 함수의 쓰임새가 콜백 함수에 국한되지 않는 경우라면 반복을 줄이기 위해 B를 외부로 분리하는 편이 나을 수 있다.
즉, fruit를 인자로 받아 출력하는 형태로 바꾸어 보자.
콜백 함수를 내부 함수로 선언해서 외부변수를 직접 참조하는 방법 (2)
var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');
var alertFruit = function (fruit) { // 추가된 코드
alert('your choice is' + fruit);
};
fruites.forEach(function (fruit) {
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', alertFruit);
$ul.appendChild($li);
});
document.body.appendChild($ul);
alertFruit(fruits[1]);
alertFruit라는 변수에 콜백 함수를 담아 외부로 꺼냈다.
이제 alertFruit을 직접 실행할 수 있다.
그런데, 각 li를 클릭하면 클릭하면 클릭한 대상의 과일명이 아닌 [object MouseEvent]라는 값이 출력된다.
콜백 함수의 인자에 대한 제어권을 addEventListener가 가진 상태이며,
addEventListener는 콜백 함수를 호출할 때 첫 번째 인자에 이벤트 객체를 주입하기 때문이다.
이 문제는 bind 메서드를 활용하면 해결된다.
bind 메서드 활용하여 값 직접 넘겨주어 클로저 발생하지 않지만 여러 제약 사항 발생한 방법
var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');
var alertFruit = function (fruit) {
alert('your choice is' + fruit);
};
fruites.forEach(function (fruit) {
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', alertFruit.bind(null, fruit)); // 추가된 코드
$ul.appendChild($li);
});
document.body.appendChild($ul);
alertFruit(fruits[1]);
다만 이렇게 하면,
이벤트 객체가 인자로 넘어오는 순서가 바뀌는 점과
함수 내부에서의 this가 원래의 this와 달라지는 점 감안해야 한다.
bind 메서드의 첫 번째 인자가 새로 바인딩할 this인데, 이 값을 생략할 수 없기 때문에 일반적으로 원래의 this를 유지하도록 할 수 없는 경우가 많다.
예제에서는 두 번째 인자에 이벤트 객체가 넘어온다.
이런 변경사항이 발생하지 않게 해결하기 위해서는 함수형 프로그래밍에서 자주 쓰이는 고차함수를 활용한다.
고차함수: 함수를 인자로 받거나 함수를 리턴하는 함수
콜백 함수를 고차 함수로 바꾸어 클로저 적극 활용 방법
var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');
var alertFruitBuilder = function (fruit) {
return function () {
alert('your choice is' + fruit);
};
};
fruites.forEach(function (fruit) {
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', alertFruitBuilder.bind(null, fruit)); // 추가된 코드
$ul.appendChild($li);
});
document.body.appendChild($ul);
alertFruit(fruits[1]);
alertFruitBuilder 함수 내부에서 기존의 alertFruit 함수인 익명 함수를 반환한다.
alertFruitBuilder 함수를 실행하면서 fruit 값을 인자로 전달한다.
이 함수의 실행 결과가 다시 함수가 되며, 이렇게 반환된 함수를 리스너에 콜백 함수로써 전달한다.
이후 클릭 이벤트가 발생하면 이 함수의 실행 컨텍스트가 열리면서 alertFruitBuilder의 인자로 넘어온 fruit를 outerEnvironmentReference에 의해 참조한다.
즉, alertFruitBuilder의 실행 결과로 반환된 함수에는 클로저가 존재한다.
위 세 방법의 장단점을 파악하고 상황에 따라 어떤 방법을 도입하는 것이 효과적일지 고민해서 적용하면 된다.
정보 은닉 information hiding
어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈 간의 결합도를 낮추고 유연성을 높이고자 하는 현대 프로그래밍 언어의 중요한 개념 중 하나
접근 권한
public : 외부에서 접근 가능
private : 내부에서만 사용, 외부에 노출되지 않음
protected :
자바스크립트는 기본적으로 변수 자체에 접근 권한을 직접 부여하도록 설계되어 있지 않다.
그렇지만 접근 권한 제어가 불가능 하지는 않다.
클로저를 이용하면 함수 차원에서 public한 값과 private한 값을 구분하는 것이 가능하다.
var outer = function () {
var a = 1;
var inner = funciton () {
return ++a;
};
return inner;
};
var outer2 = outer();
console.log(outer2());
console.log(outer2());
outer 함수를 종료할 떄 inner 함수를 반환함으로써 outer 함수의 지역변수인 a의 값을 외부에서도 읽을 수 있다.
return과 클로저를 활용하면 외부 스코프에서 함수 내부의 변수들 중 선택적으로 일부 변수에 대한 접근 권한을 부여할 수 있다.
closure의 뜻인 폐쇄성에 주목해보자.
outer 함수는 외부(전역 스코프)로부터 철저하게 격리된 닫힌 공간이다.
외부에서는 외부 공간에 노출돼 있는 outer라는 변수를 통해 outer 함수를 실행할 수는 있지만, outer 함수 내부에는 어떠한 개입도 할 수 없다.
외부에서는 오직 outer 함수가 return한 정보에만 접근할 수 있다.
return 값이 외부에 정보를 제공하는 유일한 수단이다.
외부에 제공하고자 하는 정보들을 모아서 return하고,
내부에서만 사용할 정보들은 return하지 않는 것으로 접근 권한 제어 가능하다.
return한 변수들은 공개 멤버 public member,
그렇지 않은 변수들은 비공개 멤버 private member가 된다.
간단한 자동차 경주 보드 게임 만들면서 접근 권한 제어해보자.
규칙
- 각 턴마다 주사위 굴려 나온 숫자(km) 만큼 이동한다.
- 차량별로 연료량(fuel)과 연비(power)는 무작위로 생성된다.
- 남은 연료가 이동할 거리에 필요한 연료보다 부족하면 이동하지 못한다.
- 모든 유저가 이동할 수 없는 턴에 게임이 종료된다.
- 게임 종료 시점에 가장 멀리 이동해 있는 사람이 승리한다.
규칙에 따라 간단하게 자동차 객체를 만들어보자.
간단한 자동차 객체
var car = {
fuel: Math.ceil(Math.random() * 10 + 10), // 연료(L)
power: Math.ceil(Math.random() * 3 + 2), // 연비(km/L)
moved: 0, // 총 이동거리
run: function () {
var km = Math.ceil(Math.random() * 6);
var wasteFuel = km / this.power;
if (this.fuel < wasteFuel) {
console.log('이동불가');
return;
}
this.fuel -= wasteFuel;
this.moved += km;
console.log(km + 'km 이동 (총 ' + this.moved + 'km)');
}
};
car 변수에 객체를 직접 할당한다.
fuel과 power는 무작위로 생성하고, moved라는 프로퍼티에 총 이동거리를 부여한다.
run 메서드를 실행할 때마다 car 객체의 fuel, moved 값이 변하게 한다.
이런 car 객체를 사람 수만큼 생성해서 각자의 턴에 run을 실행하면 게임을 즐길 수 있다.
모두가 run 메서드만 호출한다는 가정하에는 이정도만으로 충분하다.
승부욕이 강한 사람이 참여한다면 무작위로 정해지는 연료, 연비, 이동거리 등을 바꿀 수 있다.
car.fuel = 10000;
car.power = 100;
car.moved = 1000;
이런 식으로 마음대로 바꿔버리면 일방적인 게임이 되므로 방어할 필요가 있다.
클로저를 활용하여 객체가 아닌 함수로 만들고, 필요한 멤버만을 따로 return 해보자.
클로저로 변수를 보호한 자동차 객체 (1)
var createCar = function () {
var fuel = Math.ceil(Math.random() * 10 + 10); // 연료(L)
var power = Math.ceil(Math.random() * 3 + 2); // 연비(km/L)
var moved = 0; // 총 이동거리
return {
get moved () {
return moved;
},
run: function () {
var km = Math.ceil(Math.random() * 6);
var wasteFuel = km / power;
if (fuel < wasteFuel) {
console.log('이동불가');
return;
}
fuel -= wasteFuel;
moved += km;
console.log(km + 'km 이동 (총 ' + moved + 'km). 남은 연료: ' + fuel);
}
};
};
var car = createCar();
createCar라는 함수를 실행함으로써 객체를 생성한다.
fuel, power 변수는 비공개 멤버로 지정해 외부에서의 접근을 제한했고,
moved 변수는 getter만을 부여함으로써 읽기 전용 속성을 부여했다.
외부에서는 오직 run 메서드를 실행하는 것과 현재의 moved 값을 확인하는 두 가지 동작만 가능하다.
다음과 같이 값을 변경하고자 하는 시도는 대부분 실패한다.
car.run(); // 3km 이동 (총 3km). 남은 연료: 17.4
console.log(car.moved); // 3
console.log(car.fuel); // undefined
console.log(car.power); // undefined
car.fuel = 1000;
console.log(car.fuel); // 1000
car.run(); // 1km 이동 (총 4km). 남은 연료: 17.2
car.power = 100;
console.log(car.power); // 100
car.run(); // 4km 이동 (총 8km). 남은 연료: 16.4
car.moved = 1000;
console.log(car.moved); // 8
car.run(); // 2km 이동 (총 10km). 남은 연료: 16
비록 run 메서드를 다른 내용으로 덮어씌우는 어뷰징은 여전히 가능한 상태이긴 하지만,
앞서의 코드보다는 훨씬 안전한 코드이다.
어뷰징까지 막기 위해서는 객체를 return하기 전에 미리 변경할 수 없게끔 조취를 취한다.
클로저로 변수를 보호한 자동차 객체 (2)
var createCar = function () {
var fuel = Math.ceil(Math.random() * 10 + 10); // 연료(L)
var power = Math.ceil(Math.random() * 3 + 2); // 연비(km/L)
var moved = 0; // 총 이동거리
var publicMembers = {
get moved () {
return moved;
},
run: function () {
var km = Math.ceil(Math.random() * 6);
var wasteFuel = km / power;
if (fuel < wasteFuel) {
console.log('이동불가');
return;
}
fuel -= wasteFuel;
moved += km;
console.log(km + 'km 이동 (총 ' + moved + 'km). 남은 연료: ' + fuel);
}
};
Object.freeze(publicMembers);
return publicMembers;
};
var car = createCar();
이 정도면 충분히 안전한 객체이다.
클로저를 활용해 접근 권한을 제어하는 방법
return한다.return한 변수들은 공개 멤버가 되고, 그렇지 않은 변수들은 비공개 멤버가 된다.