클로저는 자바스크립트 고유의 개념이 아니라 여러 함수형 프로그래밍 언어에서 등장하는 보편적인 특징이다. 그래서인지 ECMAScript 명세에서도 클로저의 정의가 다뤄지지 않고 있고, 클로저에 대한 여러가지 정의와 해석이 존재한다.
"A closure is the combination of a function and the lexical environment within which that function was declard." - MDN
오... 정의를 들으니 더 어렵다. 아래 예제 코드를 보자.
1 var outer = function () {
2 var a = 1;
3 var inner = function () {
4 return ++a;
5 };
6 return inner; // inner 함수 자체를 반환
7 };
8 var outer2 = outer();
9 console.log(outer2()); // 2
10 console.log(outer2()); // 3
위를 보면, 8번째 줄에서 outer 함수의 실행 컨텍스트가 종료된 이후에도 inner 함수가 outer 함수에서 선언된 변수 a를 참조한 결과값을 출력하고 있다.
본인의 environmentRecord에 해당 내용이 없으니 outerEnvironmentReference인 outer 함수의 LexicalEnvironment를 참조하게 된 것이다. 그래서 MDN의 정의 중 'lexical environment'라는 말이 담긴 것이다.
🛒이것이 가능한 이유는 가비지 컬렉터의 동작 방식에 있다. 가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않는다. 위의 예제처럼 지역변수를 참조하는 내부함수가 외부로 전달된 경우에는 가비지 컬렉터의 수집 대상에서 제외된다.
위의 내용을 종합해 클로저의 정의를 정리해보면 다음과 같다:
✨클로저란 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상을 의미한다.
클로저는 그 본질이 메모리를 계속 차지하는 개념이므로, 더는 사용하지 않게 된 클로저에 대해서는 관리를 해줄 필요가 있다. 참조 카운트를 0으로 만들면 언젠가 가비지 컬렉터가 수거해갈 것이고, 이때 소모됐던 메모리를 회수할 수 있다.
참조 카운트를 0으로 만들기 위해서는 식별자에 참조형이 아닌 기본형 데이터(보통 null이나 undefined)를 할당하면 된다.
var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');
fruits.forEach(function (fruit) { // (A)
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', function () { // (B)
alert('your choice is ' + fruit);
});
$ul.appendChild($li);
});
document.body.appendChild($ul);
위의 코드에서 함수 (A)는 외부 변수를 사용하고 있지 않으므로 클로저가 없지만, 콜백 함수 (B)에는 fruit이라는 참조하고 있으므로 클로저가 있다. 따라서 이 변수 fruit에 대해서는 (A)가 종료된 이후에도 GC 대상에서 제외되어 계속 참조 가능할 것이다.
만약 (B)함수가 콜백 함수에 국한되지 않는다면 외부로 분리를 해줄 수 있다. 하지만 이 방식으로 코드를 작성한 후에 각 li
를 클릭하면, 클릭 대상인 과일이 출력되는 게 아니라 [Object MouseEvent]
라는 값이 출력된다. 이 현상이 나타나는 이유는, 콜백 함수의 인자에 대한 제어권을 addEventListener
가 가진 상태이며, 이 addEventListener
는 콜백 함수를 호출할 때 첫 번째 인자에 '이벤트 객체'를 주입하기 때문이다. 해결 방법은 bind
메서드를 활용하는 것이다.
var alertFruit = function (fruit) {
alert('your choice is ' + fruit);
};
fruits.forEach(function (fruit) {
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', alertFruit.bind(null, fruit));
$ul.appendChild($li);
});
다만 이렇게 하면 이벤트 객체가 인자로 넘어오는 순서가 바뀌는 점 등 함수 내부에서의 this
가 원래의 this
와 달라지는 점은 감안해야 한다. 이런 이슈를 해결하기 위해서는 고차함수를 활용할 수 있다.
💡 고차함수란? 함수를 인자로 받거나 함수를 리턴하는 함수
var alertFruitBuilder = function (fruit) {
return function () {
alert('your choice is ' + fuirt);
};
};
fruits.forEach(function (fruit) {
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', alertFruitBuilder(fruit));
$ul.appendChild($li);
});
기존의 alertFruit
함수를 익명 함수로 반환하는 alertFruitBuilder
함수를 만들었다. 이렇게 하면 이 함수의 실행 결과가 다시 함수가 되며, 이렇게 반환된 함수를 리스너에 콜백 함수로써 전달할 것이다. 이후 언젠가 클릭 이벤트가 발생하면 비로소 이 함수의 실행 컨텍스트가 열리면서 alertFruitBuilder
의 인자로 넘어온 fruit를 outerEnvironmentReference에 의해 참조할 수 있게 된다.
💡 정보 은닉(information hiding)이란? 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈간의 결합도를 낮추고 유연성을 높이고자 하는 현대 프로그래밍 언어의 주요 개념
접근 권한에는 public, private, protected 세 종류가 있다. 자바스크립트는 기본적으로 변수 자체에 접근 권한을 직접 부여하도록 설계되어 있지 않지만, 클로저와 return
을 활용하면 내부 변수들 중에 선택적으로 접근 권한을 부여할 수 있다.
return
한다.return
한 변수들은 공개 멤버(public member)가 되고, 그렇지 않은 변수들은 비공개 멤버(private member)가 된다.💡 부분 적용 함수(partially applied function)란? n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억시켰다가, 나중에 (n-m)개의 인자를 넘기면 비로소 원래 함수의 실행 결과를 얻게끔 하는 함수
var add = function () {
var result = 0;
for (var i = 0; i < arguments.length; i++) {
result += arguments[i];
}
return result;
};
var addPartial = add.bind(null, 1, 2, 3, 4, 5);
console.log(addPartial(6, 7, 8, 9, 10); // 55
addPartial
함수는 인자 5개를 미리 적용하고, 추후에 추가적으로 인자들을 전달하면 원래의 함수가 실행되는 부분 적용 함수이다. 하지만 위의 코드에서는 this
를 변경해야만 하기 때문에 메서드에서는 활용할 수 없다. this
에 관여하지 않는 별도의 부분 적용 함수가 있다면 범용성 측면에서 더 좋을 것이다.
💡 커링 함수(currying function)란? 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것
커링 함수는 위의 부분 적용 함수와 비슷해 보이지만 차이점도 존재한다. 부분 적용 함수는 여러 개의 인자를 받을 수 있고, 실행 결과를 재실행 할 때 원본 함수가 무조건 실행되는 반면, 커링 함수는 한 번에 하나의 인자만 전달하는 것이 원칙이며 중간 과정상의 함수를 실행한 결과는 그 다음 인자를 받기 위해 대기만 할 뿐, 마지막 인자가 전달되기 전까지는 원본 함수가 실행되지 않는다.
밑의 예시는 3개의 인자를 받는 커링 함수이다.
var curry3 = function (func) {
return function (a) {
return function (b) {
return func(a, b);
};
};
};
var getMaxWith10 = curry3(Math.max)(10);
console.log(getMaxWith10(8)); // 10
console.log(getMaxWith10(25)); // 25
var getMinWith10 = curry3(Math.min)(10);
console.log(getMinWith10(8)); // 8
console.log(getMinWith10(25)); // 10
커링 함수는 위처럼 필요한 인자의 개수만큼 함수를 만들어서 리턴해주면 되기 때문에 간편하지만, 인자가 많아질수록 가독성이 떨어진다는 단점이 있다. 이 문제는 화살표 함수로 해결 가능하다.
var curry5 = func => a => b => c => d => e => func(a, b, c, d, e);
커링 함수 각 단계에서 받은 인자들을 모두 마지막 단계에서 참조할 것이므로 GC되지 않고 메모리에 차곡차곡 쌓였다가, 마지막 호출로 실행 컨텍스트가 종료된 후에야 비로소 GC의 수거 대상이 될 것이다.
커링 함수는 함수형 프로그래밍에서 말하는 지연 실행에서 유용하다. 당장 필요한 정보만 받아서 전달하고 또 필요한 정보가 들어오면 전달하는 식으로 하다가 결국 마지막 인자가 넘어갈 때까지 함수 실행을 미루는 것이다.
또한 프로젝트 내에서 자주 쓰이는 함수의 매개변수가 항상 비슷하고 일부만 바뀔 때, 혹은 REST API를 이용할 경우 baseUrl은 몇 개로 고정되지만 나머지 path나 id 값은 많을 때도 유용히 쓰일 수 있다. 공통적인 요소를 먼저 기억시켜두고 특정한 값만으로 서버 요청을 수행하는 함수를 만들어둔다면 개발 효율성이나 가독성 측면에서 좋을 것이다.