///////
Search

클로저 (1)

클로저의 의미 및 원리 이해

MDN에서의 클로저 정의
클로저는 함수와 그 함수가 선언될 당시의 Lexical Environment의 상호관계에 따른 현상
선언될 당시의 Lexical EnvironmentouterEnvironmentReference
복습
컨텍스트 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 선언
내부 함수인 innerenvironmentRecord 에서 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 함수의 실행 컨텍스트가 종료되면 outer2inner 함수 참조
동작 방식
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
복사

정리

클로저란 어떤 함수에서 선언한 변수를 참조하는 내부함수를 외부로 전달할 경우, 함수의 실행 컨텍스트가 종료된 이후에도 해당 변수가 사라지지 않는 현상