클로저의 의미 및 원리 이해
클로저는 자바스크립트 고유의 개념이 아니기 때문에 ECMAScript 명세에는 클로저의 정의를 다루지 않고 있다.
MDN에서는 클로저에 대해 "주변 상태(어휘적 환경)에 대해 참조와 함께 묶인(포함된) 함수의 조합" 이라고 소개하고 있다. 이는 즉 "어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상"이라고 볼 수 있다.
먼저, 외부 함수의 변수를 참조하는 내부 함수부터 살펴보자.
var outer = function() {
var a = 1;
var inner = function() {
return ++a;
}
return inner();
};
var outer2 = outer();
console.log(outer2); // 2
- outer함수에서 변수 a 선언
- inner함수에서 a의 값 1 증가
- inner함수 내부에서는 a가 environmentRecord에 없으므로 상위 컨텍스트인 outer의 LexicalEnvironment에서 a를 찾음
- inner함수를 실행한 결과를 리턴하므로 outer함수의 실행 컨텍스트가 종료된 시점에는 a 변수를 참조하는 대상이 없어짐
- 참조하지 않는 식별자(a, inner)는 가비지 컬렉터 수집 대상이 됨
- 즉, 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 함수를 참조하게 된다. 여기서 이상한 점은 inner함수의 실행 시점에는 outer함수는 이미 실행이 종료된 상태인데 outer 함수의 LexicalEnvironment에 어떻게 접근할 수 있는걸까? 이는 가비지 컬렉터의 동작 방식 때문이다.
가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함하지 않는다. outer함수는 실행 종료 시점에 inner함수를 반환하고, 외부함수인 outer의 실행이 종료되더라도 내부 함수인 inner 함수가 언젠가 outer2를 실행함으로써 호출될 가능성이 열린 것이다. inner 함수의 실행 컨텍스트가 활성화되면 outerEnvironmentReference가 outer 함수의 LexicalEnvironment를 필요로 할 것이므로 GC 수집 대상에서 제외된다. 그 덕에 inner 함수가 이 변수에 접근할 수 있는 것이다.
이처럼 함수의 실행 컨텍스트가 종료된 후에도 LexicalEnvironment가 GC 수집 대상에서 제외되는 경우는 지역변수를 참조하는 내부함수가 외부로 전달되는 경우가 유일하다.
클로저와 메모리 관리
클로저는 필요에 의해 의도적으로 함수의 지역변수를 메모리를 소모하도록 함으로써 발생한다. 그 필요성이 사라진 시점에는 더는 메모리를 소모하지 않도록 참조 카운트를 0으로 만들면 된다. 이를 위해서는 식별자에 참조형이 아닌 기본형 데이터(보통 null이나 undefined)를 할당하면 된다.
var outer = function() {
var a = 1;
var inner = function() {
return ++a;
}
return inner;
};
console.log(outer());
outer = null; // outer 식별자의 inner 함수 참조를 끊음
클로저 활용 사례
콜백 함수 내부에서 외부 데이터를 사용하고자 할 때
대표적인 콜백 함수 중 하나인 이벤트 리스너에 관한 예시를 살펴보자.
const fruits = ['apple', 'banana', 'peach'];
const $ul = document.createElement('ul'); // (공통 코드)
fruits.forEach(function (fruit) { // (A) forEach 콜백
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', function() {// (B) 클릭 이벤트 핸들러
alert('your choice is ' + fruit); // fruit 외부 변수 참조
});
$ul.appendChild($li);
});
document.body.appendChild($ul);
(A)는 fruits의 개수만큼 실행되며, 그때마다 새로운 실행 컨텍스트가 활성화된다. (A)의 실행 종료 여부와 무관하게 클릭 이벤트에 의해 각 컨텍스트의 (B)가 실행될 때는 (B)의 outerEnvironmentReference가 (A)의 LexicalEnvironment를 참조하게 된다. 따라서 최소한 (B)함수가 참조할 예정인 변수 fruit에 대해서는 (A)가 종료된 후에도 GC 대상에서 제외되어 계속 참조 가능할 것이다.
접근 권한 제어(정보 은닉)
정보 은닉은 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈 간의 결합도를 낮추고 유연성을 높이고자 하는 현대 프로그래밍 언어의 중요한 개념 중 하나다. 흔히 접근 권한에는 public, private, protected 세 종류가 있으며, 클로저를 이용해 public한 값과 private한 값을 구분하는 것이 가능하다.
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 함수를 종료할 때 inner 함수를 반환함으로써 outer함수의 지역변수인 a의 값을 외부에서도 읽을 수 있게 됐다.
이처럼 외부에 제공하고자 하는 정보들을 모아서 return하고, 내부에서만 사용할 정보들은 return하지 않는 것으로 public member와 private member로 나눌 수 있다.
부분 적용 함수
부분 적용 함수란 n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억시켰다가, 나중에 (n-m)개의 인자를 넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수이다. this를 바인딩해야 하는 점을 제외하면 앞서 살펴본 bind 메서드의 실행 결과가 바로 부분 적용 함수다.
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개를 미리 적용하고, 추후 추가적으로 인자들을 전달하면 모든 인자를 모아 원래의 함수가 실행되는 부분 적용 함수이다.
커링 함수
커링 함수란 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것을 말한다. 위 부분 적용 함수와 기본적인 맥락은 일치하지만, 커링은 한 번에 하나의 인자만 전달하는 것을 원칙으로 한다. 또한 중간 과정상 함수를 실행한 결과는 그 다음 인자를 받기 위해 대기할 뿐, 마지막 인자가 전달되기 전까지는 원본 함수가 실행되지 않는다. (부분 적용 함수는 여러 개의 인자를 전달할 수 있고, 실행 결과를 재실행할 때 원본 함수가 무조건 실행된다.)
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 curry5 = func => a => b => c => d => func(a,b,c,d);
'개발 > 프론트엔드' 카테고리의 다른 글
[JavaScript] 코어 자바스크립트 week7 - 클래스 (0) | 2024.12.03 |
---|---|
[JavaScript] 코어 자바스크립트 week6 - 프로토타입 (0) | 2024.11.26 |
[JavaScript] 코어 자바스크립트 week4 - 콜백함수 (0) | 2024.11.11 |
[JavaScript] 코어 자바스크립트 week3 -this (0) | 2024.11.05 |
[JavaScript] 코어 자바스크립트 week2 -실행 컨텍스트 (0) | 2024.10.28 |