[JS] 클로저

2022. 8. 7. 13:11학습/JavaScript

반응형

JS 클로저

클로저

MDN에서는 클로저를 "함수와 그 함수가 선언될 당시의 Lexical environment의 상호관계에 따른 현상"으로 말하고 있습니다.

Lexical environment에 대해서 복습하자면

LexicalEnvironment는 "환경레코드", "outerEnvironmentReference"로 이루어져 있으며

outerEnvironmentReference는 함수가 선언될 당시의 LexicalEnvironment를 참조하면서 스코프 체인이 일어나게 되고 식별자의 유효 범위가 늘어납니다.

예로 들면 함수 A와 A의 내부함수 B가 있다면,

내부함수 B의 outerEnvironmentReference 에는 함수 A의 LexicalEnvironment에 접근을 하는 겁니다.

그렇다면 함수 A에서 내부함수 B는 간섭 못하지만 내부함수 B에서는 함수 A에 간섭이 가능합니다.

그렇다면 "어떤 함수(A)에서 선언한 변수를 참조하는 내부함수(B)에서만 발생하는 현상"이라고도 할 수 있습니다.

예시


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 함수 자체를 반환하는데,

inner 함수의 outerEnvironmentReference에는 outer의 LexicalEnvironment를 참조하고 있을 것입니다.

outer 함수가 종료되면서 해당 실행컨텍스트가 사라지는 게 정상이지만

inner함수의 실행컨텍스트가 활성화될 당시, inner함수의 outerEnvironmentReference에 있는 outer의 LexicalEnvironment를 필요할 수 있기 때문에 가비지 컬렉터 수집대상에서 제외가 됩니다.

그러므로 inner함수에서 outer의 실행컨텍스트가 종료됬음에도 outer의 변수에 접근할 수 있게 됩니다.

그러면 클로저는 "외부 함수의 LexicalEnvironment가 가비지 컬렉팅되지 않는 현상" 이라고도 할 수 있습니다.

메모리 관리

클로저로 인해서 가비지 컬렉팅이 안 되고 있기 때문에 메모리 소모에 대해서 관리하면 좋습니다.

클로저는 의도적으로 함수의 지역변수를 활용하여 메모리 소모를 합니다.

그럼 지역변수에 null, undefined 할당을 통해서 참조 카운트를 0으로 만들면 가비지 컬렉팅을 진행시켜 메모리 관리를 시도하면 됩니다.


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 함수 참조를 끊는다. 

클로저 활용사례

그럼 이 클로저의 현상을 어떻게 사용하는지 알아봅시다.

1. 콜백함수 내부에서 외부 데이터를 사용하고자 할 때

1.1 일반적인 경우


var fruits = ['apple', 'banana', 'peach'];
var $ul = document.createElement('ul');

fruits.forEach(function (fruit) { // (A)
    var $li = document.createElement('li');
    $li.innerText = fruit;
    $li.addEventListener('click', function () { // (B)
        alert('your choice is ' + fruit);
    });
    $ul.appendChild($li);
});
document.body.appendChild($ul);

(B)에서 Fruit을 참고하고 있기 때문에 GC 대상에서 제외되서 (A)가 종료된 이후에도 계속 참조가 가능합니다.

1.2 bind를 활용한 경우


var alertFruit = function (fruit) {
    alert('your choice is ' + fruit);
};
fruits.forEach(function (fruit) {
    var $li = document.createElement('li');
    $li.innerText = fruit;
    $li.addEventListener('click', alertFruit.bind(null, fruit)); //bind를 활용
    $ul.appendChild($li);
});
...

(B)를 외부함수로서 관리하고 싶다면 따로 꺼내서 함수로 사용할 수 있습니다.

하지만 bind메서드를 활용 해서 값을 직접 넘겨주는 식으로 활용해볼 수 있습니다.

bind를 활용하지 않는다면 어떻게 될까요?


fruits.forEach(function (fruit) {
        var $li = document.createElement('li');
        $li.innerText = fruit;
        $li.addEventListener('click', alertFruit);
        $ul.appendChild($li);
});

li 클릭 시, your choice is [object PointerEvent] 라는 과일이 아닌 이벤트 객체가 나옵니다.

왜냐하면 addEventListener는 콜백함수(alertFruit)를 호출할 때 첫 번째 인자에 '이벤트 객체'를 주입하기 때문입니다.

즉, alertFruitfruit[object PointerEvent]가 되는 것입니다.

그럼 콜백함수에서 외부데이터를 쓰기위해 bind를 활용하면 감안해야 하는 부분이 생깁니다.

  • 이벤트 객체가 인자로 넘어오는 순서가 바뀌는 점
  • 함수 내부의 this가 기존과 달라지는 점

이러한 이슈를 해결하기 위해 고차함수를 활용하는 방식이 있습니다.

1.3 고차함수를 활용하는 경우


var alertFruitBuiler = function (fruit) {
    return function () {
        alert('your choice is ' + fruit);   
    }
};
fruits.forEach(function (fruit) {
    var $li = document.createElement('li');
    $li.innerText = fruit;
    $li.addEventListener('click', alertFruitBuiler(fruit));
    $ul.appendChild($li);
});
...

2. 접근권한 제어(정보 은닉)

자바스크립트는 변수 자체에 접근권한을 직접 부여할 수 없습니다.

하지만 클로저의 특성을 사용한다면 publicprivate한 값의 구분이 가능해집니다.

이는 내부 함수에서 진행된 return을 통해서 진행됩니다.


var CardGame = {
    card_type: ['Spade','Clover','Heart','Diamond'],
    card_number: ['A',2,3,4,5,6,7,8,9,10,'J','Q','K'],
    pick: function () {
        picked_type=this.card_type[Math.floor(Math.random() * 4)];
        picked_number= this.card_number[Math.floor(Math.random() * 13)];
        console.log(picked_type,picked_number);
    }
}

card_type, card_number을 의도적으로 변경이 가능합니다.

card_type을 모두 Spade로 만들 수도 있겠죠

객체를 함수를 변경하고 필요한 값만 return 하는 식으로 접근권한을 제어합니다.

  • return한 변수들은 공개멤버가 되고

  • 그 외의 변수들은 비공개 멤버가 됩니다.


var CardGame = function () {
    var card_type= ['Spade','Clover','Heart','Diamond'];
    var card_number= ['A',2,3,4,5,6,7,8,9,10,'J','Q','K'];
    var picked_card = '';
    return {
        get picked_card () {
            return picked_card;
        },
        pick: function () {
            var picked_type = card_type[Math.floor(Math.random() * 4)];
            var picked_number = card_number[Math.floor(Math.random() * 13)];
            picked_card= `${picked_type}+${picked_number}`;
            console.log(picked_card);
        }
    };
};
var card = CardGame();
card.pick();

card.pick();을 바꿀 수 도 있지만 card_type, card_number에는 접근이 불가능해집니다.

3. 부분 적용 함수

부분 적용함수는 n개의 인자 중 몇 개만 기억시켰다가 나머지 인자들을 넘기면서 함수를 실행하는 함수입니다.

이전 bind 활용에서 부분 적용함수를 보인 적이 있습니다.

  • bind를 통한 부분 적용함수를 구현

    bind를 호출할 때 전달했던 인수 뒤에 이어서 등록됨

    
    var func = function (a, b, c, d) {
      console.log(this, a, b, c, d);  
    };
    var Subfunc = func.bind({ x: 1 }, 4, 5);
    Subfunc(6, 7); //{x: 1} 4 5 6 7
    
    var func = function (a, b, c, d) {
      console.log(this, a, b, c, d);  
    };
    
    

​ 이에관한 이슈는 this를 설정해야한다는 점입니다.

  • bind없이 부분 적용함수 구현

var partial = function () {
    var originalPartialArgs = arguments; //기존에 정의된 인수
    var func = originalPartialArgs[0];
    if (typeof func !== 'function') {
        throw new Error('첫번째 인자가 함수가 아닙니다.');
    }
    return function () {
        var partialArgs = Array.prototype.slice.call(originalPartialArgs, 1);
        console.log(partialArgs);
        var restArgs = Array.prototype.slice.call(arguments); // 새로 추가된 인수
        console.log(arguments);
        console.log(restArgs);
        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,6);
console.log(addPartial(7,8,9,10));

이러면 실행시점의 this를 그대로 반영합니다.

부분적용함수 - debounce

실무에서 부분함수를 통해서 debounce기능을 구현합니다.

디바운스는 짧은 시간 동안 동일한 이벤트가 많이 발생하는 경우 이를 전부 처리하지 않고 처음 또는 마지막에 발생한 이벤트에 대해 한 번만 처리하는 것입니다.

scroll, wheel, mousemove, resize 이벤트에 대해서 적용하기 좋습니다. 이를 구현한 lodash라이브러리가 있지만 구현은 따로 간단하게 구현이 가능합니다.


var debounce = function (eventName, func, wait) {
    var timeoutId = null;
    return function (event) {
        var self = this;
        console.log(eventName,'이벤트 발생');
        clearTimeout(timeoutId); // 대기큐 초기화
        timeoutId = setTimeout(func.bind(self, event), wait); // 새롭게 지연
    };
};

var moveHandler = function (e) {
    console.log('move 이벤트 처리');
}
var wheelHandler = function (e) {
    console.log('wheel 이벤트 처리');
}
document.body.addEventListener('mousemove', debounce('move', moveHandler,500));
document.body.addEventListener('mousewheel', debounce('wheel', wheelHandler,700));

  • debounce에서 클로저 처리되는 변수는 timeoutId, func, wait, eventName입니다.

4. 커링 함수

여러 개의 인자를 받는 함수를 하나의 인자만 받는 함수로 나눠서 순차적으로 호출될 수 있게 체인 형태로 구성한 것을 말합니다.

부분 적용 함수와 다른 점은

  • 커링함수는 한 번에 하나의 인자만 전달하는 것이 원칙이라는 점

  • 인자를 다 받지 않았을 때는 대기 상태이고 원본 함수가 실행되지 않는 점

입니다.

기본적인 구성


var curry3 = function (func) {
    return function (a) {
        return function (b) {
            return func(a,b);
        };
    };
};

var getMaxWith10 = curry3(Math.max)(10);
console.log(getMaxWith(8)); //10

하지만 예상할 수 있듯이 인자가 많아지면 수많은 들여쓰기로 인해서 가독성이 떨어질 수 있습니다.

그래서 화살표 함수를 활용하면 편합니다.

화살표함수를 활용한 Currying


var curry5 = func => a => b => c => d => func(a,b,c,d);

이렇게 마지막 인자가 넘어갈 때까지 함수 실행을 미루는 것을 함수형 프로그래밍에서 '지연실행' 이라고 합니다.

정리

클로저는

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

이러한 현상을 활용하여

  • 콜백함수 내부에서 외부데이터를 활용할 수 있습니다.

  • 데이터의 은닉화를 시도할 수 있습니다.

  • 부분 적용함수를 구현할 수 있습니다.

  • 커링함수를 구현할 수 있습니다.

이 글은 "코어자바스크립트" 내용에 기반하여 작성한 글입니다.

잘못된 부분이나 이해가 잘못된 부분에 대해서는 댓글로 써 주시면 감사하겠습니다!

반응형

'학습 > JavaScript' 카테고리의 다른 글

[JS] 클래스  (0) 2022.08.11
[JS] 프로토타입  (0) 2022.08.08
[JS] 콜백함수  (0) 2022.08.07
[JS] this  (0) 2022.08.06
[JS] 실행 컨텍스트  (0) 2022.07.31