[JavaScript] 클로저(Closure)
1. 클로저
- 여러 함수형 프로그래밍에서 등장하는 보편적인 특성
- 어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상
- 어떤 함수
A
에서 선언한 변수a
를 참조하는 내부함수B
를 외부에 전달할 경우A
의 실행 컨텍스트가 종료된 이후에도 변수a
가 사라지지 않는 현상
(1) 외부 함수의 변수를 참조하는 내부 함수 예제
var outer = function () {
var a = 1;
var inner = function () {
console.log(++a);
};
inner();
};
outer(); // 1
outer
함수에서 변수a
를 선언하고outer
내부 함수인inner
함수에서a
의 값을 1만큼 증가 시키고 출력 ⇒inner
함수 내부에서a
를 선언하지 않았기 때문에outerEnvironmentReference
에 지정된 상위 컨텍스트outer
의LexicalEnvironment
에 접근해a
를 찾음outer
함수의 실행 컨텍스트가 종료되면LexicalEnvironment
에 저장된 식별자에 대한 참조를 지우고 가비지 컬렉터 수집대상에 포함된다
(2) 외부 함수의 변수를 참조하는 내부 함수 예제
var outer = function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner();
};
var outer2 = outer();
console.log(outer2); // 2
- 이 함수도 마찬가지로 실행 컨텍스트가 종료되면 참조 대상이 없어진다
(3) 외부 함수의 변수를 참조하는 내부 함수 예제
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
함수의 LexicalEnvironment
접근 가능 이유
- 가비지 컬렉터의 동작 방식 때문 ⇒ 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집대상에 포함시키지 않음
- 따라서
outer
의 실행이 종료되도inner
함수는 언젠가outer2
를 실행함으로써 호출될 가능성이 열린다 inner
함수의 실행 컨텍스트가 활성화되면outerEnvironmentReference
가outer
함수의LexicalEnvironment
를 필요로 하면서 수집 대상에서 제외됌
(4) return없이 클로저가 발생하는 경우
(function () {
var a = 0;
var intervalID = null;
var inner = function () {
if (++a >= 10) {
clearInterval(intervalID);
}
console.log(a);
};
intervalID = setInterval(inner, 1000);
})();
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
// 10
- setInterval에 전달할 콜백 함수 내부에서 지역 변수를 참조 ⇒ 지역변수를 참조하는 내부함수를 외부에 전달함
2. 클로저 메모리 관리
- 객체 지향 및 함수형에서 매우 중요한 개념임
메모리 누수
는 개발자의 의도와 달리 참조 카운트가 0이 되지 않아서Gabage Collector
의 수거 대상이 되지 않는 경우이다
참조 카운트를 0으로 만드는 방법은 식별자에 참조형이 아닌 기본형 데이터(null
또는 undefined
)를 할당함
(1) return에 의한 클로저의 메모리 해제
var outer = (function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner;
})();
console.log(outer());
console.log(outer());
console.log(outer);
outer = null; // outer 식별자에 null값을 직접 할당하여 inner 함수의 참조를 끊음
console.log(outer);
2
3
[Function: inner]
null
[Done] exited with code=0 in 0.054 seconds
(2) setInterval에 의한 클로저의 메모리 해제
(function () {
var a = 0;
var intervalID = null;
var inner = function () {
if (++a >= 10) {
clearInterval(intervalID);
inner = null; // inner 식별자에 null값을 할당하여 함수 참조를 끊음
}
console.log(a);
};
intervalID = setInterval(inner, 1000);
})();
1
2
3
4
5
6
7
8
9
10
[Done] exited with code=0 in 10.05 seconds
(3) eventListener에 의한 클로저의 메모리 해제
(function () {
var count = 0;
var button = document.createElement("button");
button.innerText = "click";
var clickHandler = function () {
console.log(++count, "time clicked");
if (count >= 10) {
button.removeEventListener("click", clickHandler);
clickHandler = null; // clickHandler 식별자에 null값을 할당하여 함수 참조를 끊음
}
};
})();
3. 클로저 활용
(1) 콜백 함수 내부에서 외부 데이터 가져올때
var fruit = ["apple", "banana", "peach"];
var $ul = document.createElement("ul");
var alertFruitBuilder = function (fruit) {
return function () {
alert(`your choice is ${fruit}`);
};
};
fruit.forEach(function (fruit) {
var $li = document.createElement("li");
$li.innerText = fruit;
$li.addEventListener("click", alertFruitBuilder(fruit));
$ul.appendChild($li);
});
- 함수 내부에서 익명 함수를 반환
- 함수의 실행 결과가 함수가 되고 반환된 함수를 콜백 함수로 전달
- 클릭 이벤트가 발생하면 실행 컨텍스트가 열리고 인자로 넘어온 fruit을 outerEnvironmentReference에 참조가 가능해짐
(2) 정보 은닉 (접근 권한 제어)
자동차 경주 게임
- 각 턴마다 주사위를 굴려 나온 숫자 (
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 / power;
if (fuel < wasteFuel) {
console.log("이동 불가능");
return;
}
fuel -= wasteFuel;
moved += km;
console.log(`${km} km 이동 (총 ${moved} km) 남은 연료: ${fuel}`);
},
};
자동차 객체를 만들어서 실행시켰지만 누군가 어뷰징을 사용하면 무작위 값이 변경된다.
car.fuel = 1000; // 어뷰징으로 연료에 1000 할당 console.log("어뷰징 후 연료", car.fuel); car.run(); car.power = 100; // 어뷰징으로 연비에 1000 할당 console.log("어뷰징 후 연비", car.power); car.run(); car.moved = 1000; // 어뷰징으로 거리를 1000 할당 console.log("어뷰징 후 총 이동거리", car.moved);
- 어뷰징을 했을때 결과값 변경
어뷰징 후 연료 1000 1 km 이동 (총 1 km) 남은 연료: 999.75 어뷰징 후 연비 100 2 km 이동 (총 3 km) 남은 연료: 999.73 어뷰징 후 총 이동거리 1000
이럴때 클로저를 활용해서 객체가 아닌 함수로 만들어주고 필요한 멤버만
return
하는 방법으로 만들어야 한다createCar
함수를 실행하여 객체를 생성하고fuel
,power
변수는 비공개 멤버로 지정해서 외부 접근을 제한moved
변수는getter
만 부여하여 읽기 전용 속성을 부여
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();
car.run();
console.log(car.moved);
console.log(car.fuel);
console.log(car.power);
car.fuel = 1000; // 어뷰징으로 연료에 1000 할당
console.log("어뷰징 후 연료", car.fuel);
car.run();
car.power = 100; // 어뷰징으로 연비에 1000 할당
console.log("어뷰징 후 연비", car.power);
car.run();
car.moved = 1000; // 어뷰징으로 거리를 1000 할당
console.log("어뷰징 후 총 이동거리", car.moved);
car.run();
어뷰징 후 연료 undefined
1 km 이동 (총 4 km) 남은 연료: 17.666666666666668
어뷰징 후 연비 undefined
6 km 이동 (총 10 km) 남은 연료: 15.666666666666668
어뷰징 후 총 이동거리 10
2 km 이동 (총 12 km) 남은 연료: 15.000000000000002
- 객체를 리턴하기 전에 미리 변경 할 수 없게 만듬
(3) 부분 적용 함수
- n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억시켰다가, 나중에 (n-m)개의 인자를 넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수
실무에선 주로 디바운스에서 자주 사용
- 짧은 시간 동안 동일 이벤트가 많이 발생할 경우 이를 전부 처리하지 않고 처음 또는 마지막 발생 이벤트에 대해 한번만 처리
scroll
,wheel
,mousemove
,resize
등에 적용하기 좋음
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("마우스 이벤트");
};
var wheelHandler = function (e) {
console.log("마우스 휠 이벤트");
};
document.body.addEventListener("mousemove", debounce("move", moveHandler));
document.body.addEventListener("mousewheel", debounce("wheel", wheelHandler));
eventName
, 실행 함수(func
), 마지막 이벤트 여부 판단을 위한wait
을 받음setTimeout
을 사용하기 위해this
를 별도의 변수로 담고clearTimeout
으로 대기큐를 초기화setTimeout
으로wait
시간만큼 지연 시키고 원래의func
를 호출함- 따라서
wait
시간이 경과하기 이전에 동일한event
가 발생하면 대기열을 초기화 시키고timeoutId
에 새로운 대기열을 등록함 ⇒ 마지막 발생 이벤트만이 초기화 되지 않고 실행됌
(4) 커링 함수
- 여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성 한 것
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과 8 큰 값 10
console.log(getMaxWith10(25)); // 10과 25 큰 값 25
var getMaxWith10 = curry3(Math.min)(10);
console.log(getMaxWith10(8)); // 10과 8 작은 값 8
console.log(getMaxWith10(25)); // 10과 25 작은 값 10
10
25
8
10
[Done] exited with code=0 in 0.056 seconds
커링 함수의 장단점
장점
- 마지막 단계에서 참조 하므로 GC 되지 않고 메모리에 쌓였다가 마지막 호출로 실행 컨텍스트가 종료될 때 한번에 GC 수거 대상이 됌
단점
- 인자가 많을 수록 가독성이 떨어짐
활용
var getInformation = function (baseUrl) {
// 서버에 요청할 주소
return function (path) {
// path 값
return function (id) {
// id 값
return fetch(`${baseUrl}${path}/${id}`); // 실제 서버에 정보 요청
};
};
};
ES6 문법
var getInformation = (baseUrl) => (path) => (id) =>
fetch(`${baseUrl}${path}/${id}`);
Redux Middleware
// Redux Middleware
const thunk = (store) => (next) => (action) => {
return typeof action === "function"
? action(dispatch, store.getState)
: next(action);
};
Redux logger나 thunk에 store,next 미리 넘겨서 반환 함수를 저장하고 이후에 action만 받아서 처리가능하다