Closure
1. 클로저의 의미 및 원리 이해
클로저를 이해하기 위해 한문장으로 요약한 부분들을 모아보면
- 자신을 내포하는 함수의 컨텍스트에 접근 할 수 있는 함수
- 함수가 특정 스코프에 접근할 수 있도록 의도적으로 그 스코프에서 정의하는 것
- 함수를 선언할 때 만들어지는 유효범위가 사라진 후에도 호출할 수 있는 함수
- 이미 생명 주기상 끝난 외부 함수의 변수를 참조하는 함수
- 자유변수가 있는 함수와 자유변수를 알 수 있는 환경의 결합
- 로컬 변수를 참조하고 있는 함수 내의 함수
- 자신이 생성될 때의 스코프에서 알 수 있었던 변수들 중 언젠가 자신이 실행될 때 사용할 변수들만을 기억하여 유지시키는 함수
위와 같이 요약 및 정의 할 수 있는데 각 정의에서 중요한 부분만 뽑아 합쳐서 이해 한다면 클로저를 이해하는데 좀 더 도움이 되지 않을까 생각이 된다.
클로저는 함수와 그 함수가 선언될 당시의 Lexical Environment의 상호관계에 따른 현상 정도로 정의할 수 있는데
Lexical Environment는 이전에 공부한 실행 컨텍스트의 구성 요소 중 하나인 outerEnvironmentReference에 해당한다
Lexical Environment의 environmentRecord와 outerEnvironmentReference에 의해 스코프가 결정 되고 스코프 체인이 가능해진다
이 때 어떤 컨텍스트 A안에 선언된 내부함수 B의 실행 컨텍스트가 outerEnvironmentReference의 스코프 체인을 통해
A를 참조 하는것을 위에서 말한 상호관계에 해당 한다고 할 수 있다.
※ 예를 들어
var outer = function() {
var a = 1;
var inner = function() {
console.log(++a);
};
inner();
}
outer();
위 코드에서 outer 함수에 변수 a 를 선언했고 내부함수인 inner에서 a 값을 증가시킨 후 출력하면
inner 함수의 environmentRecord 에서는 값을 찾지 못하므로 outerEnvironmentReference에 지정된 상위 스코프인 outer의 Lexical Environment에 접근해 a 를 찾게 되고 4번째 줄에서 2가 출력 되고
outer 함수의 실행 컨텍스트가 종료 되면 Lexical Environment에 저장된 식별자들(a, 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 함수 자체를 반환하게 되면 outer 함수의 실행 컨텍스트가 종료될 때 outer2 변수는 outer의 실행결과인 inner 함수를 참조하게 된다 inner 함수 안에서 리턴하는 a는 inner함수의 environmentRecord 에서는 값을 찾지 못하므로 outerEnvironmentReference 에는 inner 함수가 선언된 outer함수의 Lexical Environment가 참조복사 되고 스코프 체이닝에 따라 outer의 변수 a에 접근해 증가시킨 후 그 값을 반환하고 inner의 실행 컨텍스트가 종료된다 재호출시 똑같은 과정을 거쳐 값이 증가해서 값을 반환하게 된다.
여기서 알아야 할 점은 inner 함수를 실행하는 시점에 이미 outer 함수는 실행이 종료 된 상태인데 어떻게 outer함수의 Lexical Environment 에 접근할 수 있는지이다.
이는 가비지 컬렉터의 동작 방식 때문이다
가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있으면 그 값은 수집 대상에 포함 시키지 않는다
위의 코드에서 outer의 실행이 종료 되더라도 outer2에 outer에서 반환된 inner 함수가 할당되므로써 언젠가
inner함수가 호출될 가능성이 생기기 때문에 가비지 컬렉터의 수집대상에서 제외된다.
outer2를 실행 하므로써 inner함수의 실행 컨텍스트가 활성화 된다면 outerEnvironmentReference 가 outer의 Lexical Environment 를 필요로 할 것이기 때문이다.
따라서 closure란 어떤 함수 A에서 선언한 변수a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상으로 정의 할 수 있다.
※ 한가지 주의할 점은 "외부로 전달" 이 return 만을 의미하는 것이 아니라는 점이다
// (1) setInterval / setTimeout
(function () {
var a = 0;
var intervalId = null;
var inner = function () {
if (++a >= 10) {
clearInterval(intervalId);
}
console.log(a);
};
intervalId = setInterval(inner, 1000);
})();
// (2) eventListener
(function () {
var count = 0;
var button = document.createElement("button");
button.innerText = "click";
button.addEventListener("click", () => {
console.log(++count, "times clicked");
});
document.body.appendChild(button);
})();
첫번째 코드는 외부객체인 window의 메서드에 전달할 콜백 함수 내부에서 함수 내부의 지역 변수를 참조한다
두번째 코드는 별도의 외부객체인 DOM의 메서드에 등록할 handler 내부에서 지역 변수를 참조한다
두 코드 모드 지역 변수를 참조하는 내부함수를 외부로 전달했기 때문에 클로저이다
2. 클로저와 메모리 관리
클로저는 의도치 않은 메모리 누수 때문에 사용을 지양해야 한다는 말이 있기도 하다
하지만 메모리 누수란 표현은 의도 되지 않은 메모리 사용을 뜻하므로
의도적으로 설계한 클로저는 관리법만 잘 파악한다면 적절히 사용하는 것이 좋다.
var outer = (function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner;
})();
outer();
outer();
outer = null; // null을 재할당해 참조를 끊어준다
위 코드 처럼 클로저를 사용 후 목적을 다한다면 null 이나 undefined를 할당해 참조를 끊어준다면 메모리 관리를 할 수있다.
3. 클로저 접근 권한 제어
정보 은닉은 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈간의 결합도를 낮추고 유연성을 높이고자 하는 개념이다
접근 권한에는 public, private, protected 세 종류가 있다
단어 그대로 public은 외부에서 접근 가능한 것이고 private은 내부에서만 사용하여 노출되지 않는것이다.
클로저를 이용하면 함수 차원에서 public과 private한 값을 구분하는게 가능하다
var car = {
fuel: Math.ceil(Math.random() * 10 + 10),
power: Math.ceil(Math.random() * 3 + 2),
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 = 10000
car.power = 100
car.moved = 1000 // ----> 이런식으로 속성을 마음대로 바꾼다면
객체에 속성 값을 마음대로 바꿀 수 있다면 게임핵을 쓰는것과 다름없는 상황이 될 것이다.
이 때 클로저를 활용 할 수 있다
객체를 함수로 바꾸고 필요한 값만 리턴하게 하는 것이다.
var carGame = function() {
var fuel = Math.ceil(Math.random() * 10 + 10)
var power = Math.ceil(Math.random() * 3 + 2)
var moved = 0;
const public = {
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}`)
}
}
return Object.freeze(public) // ==> 변경 불가능한 객체 리턴
}
var car = carGame()
car.run() // 3km 이동(총 3km) 남은 연료 17.4
console.log(car.moved) // 3
console.log(car.fuel) // undefined
console.log(car.power) // undefined
car.run = function(...) <=== 덮어 씌우기 불가
위 코드 처럼 Object.freeze 를 사용하면 변경 불가능한 객체를 리턴 해줄 수 있다
3.1 부분 적용 함수
부분 적용 함수란 n개의 인자를 받는 함수에 m개의 인자를 미리 넘겨서 기억시켰다가 나머지 인자를 나중에 넘겨서 실행 결과를 받는 함수이다
실무에서 적용하기 적합한 예제인 디바운스 코드를 살펴보면 eventName,func,wait 세개 인자를 참조시킨 클로저를 반환하는 것을 볼 수 있다
var debounce = function (eventName, func, wait) {
var timeoutId = null;
return function (event) {
var self = this;
console.log(eventName, "event 발생");
clearTimeout(timeoutId);
timeoutId = setTimeout(func.bind(self, event), wait);
};
};
var moveHandler = function (e) {
console.log("move event 처리");
};
var wheelHandler = function (e) {
console.log("wheel event 처리");
};
document.body.addEventListener("mousemove", debounce("move", moveHandler, 500));
document.body.addEventListener("mousewheel", debounce("wheel", wheelHandler, 700));
3.2 커링 함수
커링 함수란 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인형태로 구성한 것을 말한다
위 의 부분적용 함수와 다른점은 커링은 한 번에 하나의 인자만 전달하는 것을 원칙으로 한다.
중간 과정상의 함수를 실행한 결과는 그 다음 인자를 받기 위해 대기만 할 뿐 마지막 인자가 전달되기 전까지 원본 함수가 실행 되지 않는다.
※ 커링 함수 예시
var curry = function (func) {
return function (a) {
return function (b) {
return func(a, b);
};
};
};
var getMaxWith10 = curry(Math.max)(10);
console.log(getMaxWith10(8)); // ====> 10
console.log(getMaxWith10(25)); // ====> 25
커링 함수가 유용한 경우는 당장 필요한 정보만 받아서 전달하고 또 필요한 정보가 들어오면 전달하는 식으로 마지막 인자가 넘어갈 때까지 함수 실행을 미루는 지연실행이 필요할때이다
var getInformation = function (baseUrl) {
return function (path) {
return function (id) {
return fetch(baseUrl + path + "/" + id);
};
};
};
// ES6
var getInformation = (baseUrl) => (path) => (id) => fetch(baseUrl + path + "/" + id);
위 코드에서처럼 fetch 함수는 자주 변하지 않는 url을 먼저 받아 놓고 자주 변하는 path나 id 값이 변할때 마다 값을 리턴받아 사용할 수 있다
※ 대표적인 사용 예시
// Redux Middleware 'Logger'
const logger = (store) => (next) => (action) => {
console.log("dispatching", action);
console.log("next state", store.getState());
return next(action);
};
// Redux Middleware 'thunk'
const thunk = (store) => (next) => (action) => {
return typeof action === "function"
? action(dispatch, store.getState())
: next(action);
};
4. 결론
1. 클로저란 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우 함수의 실행 컨텍스트가 종료된 후에도 해당 변수가 사라지지 않는 현상이다.
2. 내부함수를 외부로 전달하는 방법에는 return 하는 방법뿐 아니라 콜백으로 전달하는 방법도 있다.
3. 클로저는 본질이 메모리를 계속 차지하는 개념이므로 필요없는 클로저에 대해서는 메모리를 차지하지 않도록 관리 해줘야 한다.