클로저의 의미 및 원리 이해
•
MDN에서의 클로저 정의
◦
클로저는 함수와 그 함수가 선언될 당시의 Lexical Environment의 상호관계에 따른 현상
◦
선언될 당시의 Lexical Environment → outerEnvironmentReference
◦
복습
▪
컨텍스트 A에서 선언한 내부함수 B의 실행 컨텍스트가 활성화된 시점에는 B의 outerEnvironmentReference가 컨텍스트 A의 Lexical Environment에 접근이 가능
→ 스코프 체인
◦
함수와 그 함수가 선언될 당시의 Lexical Environment의 상호관계의 의미
→ 내부함수에서 외부 변소를 참조하는 경우!
•
클로저 1차 정의
◦
어떤 함수에서 선언한 변수를 참조하는 내부 함수에서만 발생하는 현상
1.
내부 함수가 외부 함수의 변수를 참조
var outer = function () {
var a = 1;
var inner = function () {
console.log(++a);
};
inner();
};
outer();
JavaScript
복사
◦
외부 함수인 outer 에서 변수 a 선언
◦
내부 함수인 inner 는 environmentRecord 에서 a 를 찾지 못해서 outerEnvironmentReference 를 통해 outer 함수에서 a 를 참
→ 일반적인 동작..
2.
내부 함수가 외부 함수의 변수를 참조 (return), 외부 함수가 내부 함수의 결과값을 return
var outer = function () {
var a = 1;
var inner = function () {
return ++a;
};
return inner();
};
var outer2 = outer();
console.log(outer2);
JavaScript
복사
◦
outer 의 실행 컨텍스트가 종료된 이후에도 inner 함수를 호출할 수 있게 하려면 어떻게 해야할까?
3.
내부 함수 자체를 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
JavaScript
복사
◦
outer 함수의 실행 컨텍스트가 종료되면 outer2 는 inner 함수 참조
◦
동작 방식
1.
inner 함수의 environmentRecord 에서 수집할 정보가 없다.
2.
inner 함수의 outerEnvironmentRecord 는 외부 함수인 outer 함수의 Lexical Environment 를 담는다.
3.
스코프 체인에 따라서 변수 a 에 접근한다.
◦
outer 의 실행 컨텍스트가 종료됐음에도 outer 함수의 Lexical Environment 에 계속 접근한다.
→ 가비지 컬렉터의 동작 방식 때문 (참조하는 변수가 있다면 수집 대상에 포함시키지 않음)
→ 이러한 현상은 지역변수를 참조하는 내부함수가 외부로 전달된 경우가 유일하다.
•
클로저 최종 정의
◦
클로저란, 어떤 함수 A에서 선언한 변수 a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상
◦
내부함수를 외부로 전달하는 방식
▪
return
▪
window의 메서드에 전달할 콜백 함수 내부에서 지역변수 참조
▪
즉시 실행 함수
클로저와 메모리 관리
•
메모리 누수
◦
개발자의 의도와 달리 어떤 값의 참조 카운트가 0이 되지 않아 가비지 컬렉터의 수거 대상이 되지 않는 경우
•
클로저는 의도적으로 함수의 지역변수를 메모리를 소모하도록 함으로써 발생
◦
필요성이 사라진 시점에는 더는 메모리를 소모하지 않게 해주면 관리가 가능 → 참조 카운트를 0으로 만들기 → 기본형 데이터 (null, undefined) 할당
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 함수 참조 끊기
JavaScript
복사
클로저 활용 사례
1. 콜백 함수 내부에서 외부 데이터를 사용하고자 할 때
•
이벤트 리스너
var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');
fruits.forEach(function (fruit) {
var $li = document.createElement('ul');
$li.innerText = fruit;
$li.addEventListener('click', function () {
alert('your choice is ' + fruit);
});
$ul.appendChild($li);
});
document.body.appendChild($ul);
JavaScript
복사
1.
이벤트리스너가 외부 변수인 fruit 을 참조하고 있으므로 클로저가 존재
•
이벤트리스너의 콜백 함수를 분리해보기
var alertFruit = function (fruit) {
alert('your choice is ' + fruit);
};
fruits.forEach(function (fruit) {
var $li = document.createElement('ul');
$li.innerText = fruit;
$li.addEventListener('click', alertFruit);
$ul.appendChild($li);
});
document.body.appendChild($ul);
alertFruit(fruits[1]);
JavaScript
복사
1.
해당 코드를 통해 li 를 클릭하면 과일명이 아닌 [object MouseEvent] 가 출력된다.
2.
그 이유는, 이벤트리스너가 인자에 대한 제어권을 가진 상태이며, addEventListener 는 콜백 함수를 호출할 때 첫 번째 인자에 이벤트 객체를 주입하기 때문 → bind 메서드로 해결 가능
•
문제점 → this가 달라진다.
•
해결방법 → 고차함수를 활용한다.
◦
고차함수 → 함수를 인자로 받거나, 함수를 리턴하는 함수
var alertFruitBuilder = function (fruit) {
return function () {
alert('your choice is ' + fruit);
}; // return된 함수가 fruit 참조 -> 클로저
};
fruits.forEach(function (fruit) {
var $li = document.createElement('li');
$li.innerText = fruit;
$li.addEventListener('click', alertFruitBuilder(fruit));
$ul.appendChild($li);
});
JavaScript
복사
•
콜백 함수 내부에서 외부 변수를 참조하기 위한 방법
1.
콜백 함수를 내부 함수로 선언해서 외부변수를 직접 참조 → 클로저
2.
bind 메서드 활용 → 클로저 발생 x, 제약사항 생김
3.
콜백 함수를 고차 함수로 바꿔서 클로저 적극적 활용
2. 접근 권한 제어 (정보 은닉)
•
정보 은닉
◦
어떤 모듈의 내부 로직에 의해 외부로의 노출을 최소화해서 모듈간의 결합도를 낮추고 유연성을 높이고자 하는 것
•
외부 공간에서 선언된 outer 함수를 호출할 수 있지만, 내부에는 개입할 수 없습니다.
◦
공개하고자 하는 것들은 return으로 반환하고
◦
보호하고자 하는 것들은 return 하지 않는다.
•
자동차 경주 게임 만들어보기
// 규칙
// 각 턴마다 주사위를 굴려 나온 숫자만큼 이동
// 차량별로 연료랑, 연비는 무작위로 생성
// 남은 연료가 이동할 거리에 필요한 연료보다 부족하면 이동 불가능
// 모든 유저가 이동할 수 없는 턴에 게임 종료
// 게임 종료 시점에 가장 멀리 이동해 있는 사람이 승리
JavaScript
복사
1.
자동차 객체 생성
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)');
}
};
JavaScript
복사
→ fuel , power , moved 는 오직 run 메서드만으로 제어가 되어야한다.
→ 따라서, 클로저를 활용!
→ 객체가 아닌 함수로 만들고, 필요한 것들만을 return 하는 방식을 채택!
2.
클로저로 변수를 보호한 자동차 객체 생성
var createCar = function () {
// 비공개 프로퍼티
var fuel = Math.ceil(Math.random() * 10 + 10);
var power = Math.ceil(Math.random() * 3 + 2);
var moved = 0;
// 공개 메서드 getter -> moved는 읽기 전용
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();
JavaScript
복사
3. 부분 적용 함수
•
부분 적용 함수
◦
n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 기억시켰다가, 나중에 나머지 인자를 넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수
•
부분 적용 함수 예시 1 - 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); // 인자 5개를 미리 적용
console.log(addPartial(6, 7, 8, 9, 10)); // 55 앞의 인자들을 모두 모아 함수 실행
JavaScript
복사
→ this를 바인딩해야하는 단점이 있다.
•
this 에 관여하지 않는 별도의 부분 적용 함수
var partial = function () {
var originalParitalArgs = arguments;
var func = originalPartialArgs[0]; // 첫 번째 인자
if (typeof func !== 'function') {
throw new Error('첫 번째 인자가 함수가 아닙니다.');
}
return function () {
// 첫 번째 인자를 제외하고 call을 통해 유사배열객체를 객체로
var partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
// 내부 함수로 들어오는 인자를 담은 유사배열객체를 객체로
var restArgs = Array.prototype.slice.call(arguments);
// 내부 함수로 들어오는 인자들을 func의 인자로 전달 (배열이므로 apply를 사용하여 전달)
return func.apply(this, partialArgs.concat(restArgs));
};
};
var add = function () {
var result = 0;
for (var i = 0; i < arguments.length; i++) {
result += arguments[i];
}
return result;
};
var addPartial = partial(add, 1, 2, 3, 4, 5);
console.log(addPartial(6, 7, 8, 9, 10)); // 55
var dog = {
name: '강아지',
greet: partial(function(prefix, suffix) {
return prefix + this.name + suffix;
}, '왈왈, ')
};
dog.greet('입니다!');
JavaScript
복사
•
디바운스 - 짧은 시간 동안 동일한 이벤트가 많이 발생한 경우 이를 전부 처리하지 않고 처음 또는 마지막에 발생한 이벤트에 대해 한 번만 처리하는 것
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));
JavaScript
복사
4. 커링 함수
•
커링 함수
◦
여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것 → 한 번에 하나의 인자만 전달하는 것이 원칙
•
부분 적용 함수 vs 커링 함수
◦
부분 적용 함수
1.
여러 개의 인자를 전달할 수 있다.
2.
실행 결과를 재실행할 때 원본 함수가 무조건 실행
◦
커링 함수
1.
한 번에 하나의 인자만 전달한다.
2.
마지막 인자가 전달되기 전까지는 원본 함수가 실행되지 않는다.
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
JavaScript
복사
•
필요한 상황에 직접 만들어 쓰기 용이
•
But, 인자가 많아질수록 return이 많아지므로 그만큼 가독성이 떨어진다. → 화살표 함수로 대처
var curry5 = func => a => b => c => d => e => func(a, b, c, d, e);
JavaScript
복사
•
마지막 인자인 e를 넘겨주면 func이 실행
•
각 단계의 인자들은 마지막에 참조되므로 GC의 수거 대상이 되지 않는다.
•
지연실행이 가능하다!
•
Redux의 미들웨어에서 많이 사용하는 방식이다.
const logger = store => next => action => {
console.log('dispatching', action);
console.log('next state', store.getState());
return next(action);
};
// thunk
const thunk = store => next => action => {
return typeof action === 'function' ? action(dispatch, store.getState) : next(action);
};
JavaScript
복사
정리
•
클로저란 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 이후에도 해당 변수가 사라지지 않는 현상