본문으로 바로가기

5장 클로저

category 코어 자바스크립트 2020. 12. 23. 17:19

클로저의 의미 및 원리 이해

클로저란

  • 어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상

외부함수의 변수를 참조하는 내부함수1 (클로저 발생 x)

var outer = function () {
    var a = 1;
    var inner = function () {
        return ++a;
    };
    return inner();
};

var outer2 = outer();
console.log(outer2)  // 2
console.log(outer2)  // 2

outer함수에서 변수 a를 선언 했고, outer 내부함수인 inner함수에서 a의 값을 1만큼 증가시킨후 리턴한다

inner함수 내부에서는 a를 선언하지 않았기 때문에 environmentRecord에서 값을 찾지 못하므로 outerEnvironmentReference에 지정된  상위 컨텍스트인 outer의 LexicalEnvironment에 접근해서 다시 a를 찾는다

그래서 4번째 줄에서는 2가 출력된다

outer함수의 실행 컨텍스트가 종료되면 LexicalEnvironment에 저장된 식별자들 (a, inner)에 대한 참조를 지운다

그러면 각 주소에 저장돼 있던 값들은 자신을 참조하는 변수가 하나도 없게 되므로 가비지 컬렉터의 수집대상이 될 것이다

 

외부함수의 변수를 참조하는 내부함수2 (클로저 발생 o)

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함수의 실행결과가 아닌 inner함수 자체를 반환했다

그러면 outer함수의 실행컨텍스트가 종료될때(8번째 줄) outer2변수는 outer의 실행 결과인 outer의 실행 결과인 inner 함수를 참조하게 될 것이다

이후 9번째에서 outer2를 호출하면 앞서 반환된 함수인 inner가 실행된다

 

inner함수의 실행컨텍스트의 environmentRecord에는 수집할 정보가 없다 outer-EnvironmentReference에는inner함수가  선언된 위치의 LexicalEnvironment가 참조복사된다, inner함수는 outer 함수 내부에서 선언됐으므로, outer 함수의 LexicalEnvironment가 담길 것이다

이제 스코프 체이닝에 따라 outer에서 선언한 변수 a에 접근해서 1만큼 증가시킨 후 그 값인 2를 반환하고, inner 함수의 실행 컨텍스트가 종료된다

 

inner함수의 실행 시점에 outer 함수는 이미 실행이 종료된 상태인데 outer함수의 LexicalEnvironment에 어떻게 접근할 수 있는 것일까?

이유는 가비지 컬렉터의 동작 방식 때문이다, 가비지 컬렉터는 어떤 값을 참조하는 변수가 하나라도 있다면 그 값은 수집 대상에 포함시키지 않는다

inner함수를 반환하는 outer함수는 실행이 종료되더라도 내부함수인 inner함수는 언젠가 outer2를 실행함으로써 호출된 가능성이 열린 것이다

언젠가 inner함수의 실행 컨텍스트가 활성화되면 outerEnvironmentReference가 outer 함수의 LexicalEnvironment를 필요로 할 것이므로 수집대상에서 제외된다, 그 덕에 inner함수가 이 변수에 접근할수 있는 것이다

 

클로저 발생 시의 콜스택 흐름

 

앞서 클로저가

  • '어떤 함수에서 선언한 변수를 참조하는 내부함수에서만 발생하는 현상' 이라고 했는데 이를 고쳐보면 
  • '외부 함수의 LexicalEnvironment가 가비지 컬렉팅되지 않는 현상' 이라고 볼 수 있고 또 이를 고쳐보면
  • '어떤 함수 A에서 선언한 변수  a를 참조하는 내부함수 B를 외부로 전달할 경우 A의 실행 컨텍스트가 종료된 이후에도 변수 a가 사라지지 않는 현상' 이라고 볼 수 있다

 

저자는 클로저에 대해 다음 세 표현이 가장 근접하다고 했다

클로저
함수를 선언할 때 만들어지는 유효범위가 사라진 후에도 호출할 수 있는 함수
이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 함수
자신이 생성될 때의 스코프에서 알 수 있었던 변수들 중 언젠가 자신이 실행될 때 사용할 변수들만을 기억하여 유지시키는 함수

하지만 위의 표현에서도 클로저를 함수라고 정의하는 부분은 살짝 아쉽다고 한다

클로저 현상이 발견되는 함수 자체를 클로저라고 칭해도 의미는 통하지만

개념적으로 클로저는 어떤 상황에서만 발생하는 특수한 '현상' 을 의미하기 때문이다

 

 

여기서 한가지 주의할 점은 '외부로 전달'이 곧 return 만을 의미하는 것은 아니라는 점이다

//별도의 외부 객체인 window의 메서드 setInterval을 사용한 경우
(function () {
    var a = 0;
    var intervalId = null;
    var inner = function () {
        if(++a > 10) {
            clearInterval(intervalId);
        }
        console.log(a);
    };
    intervalId = setInterval(inner, 1000);
})();
//별도의 외부객체인 DOM의 메서드 addEventListener를 사용한 경우
(function () {
    var count = 0;
    var button = document.createElement('button');
    button.innerText = 'click';
    button.addEventListener('click', function () {
        console.log(++count, 'times clicked');
    });
    document.body.appendChild(button);
})();

위의 두 상황 모두 지역변수를 참조하는 내부함수를 외부에 전달했기 때문에 클로저이다

 

 

클로저와 메모리 관리

메모리 소모는 클로저의 본질적인 특성일 뿐이다

오히려 이러한 특성을 정확히 이해하고 잘 활용하도록 노력해야한다

과거에는 의도치 않게 누수가 발생하는 여러상황들이 있었지만 최근의 자바스크립트 엔진에서는 거의 발견하기 힘들어 졌으므로 이제는 의도대로 설계한 '메모리 소모'에 대한 관리법만 잘 파악해서 적용하는 것으로 충분하다

 

 

관리하는 방법 

클로저는 어떤 필요에 의해 의도적으로 함수의 지역변수를 메모리를 소모하도록 함으로써 발생하니 그 필요성이 사라진 시점에 메모리를 소모하지 않게 해주면 된다

 

클로저의 메모리 관리

var outer = (function () {
    var a = 1;
    var inner = function () {
        return ++a;
    };
    return inner;
})();
console.log(outer());  // 2
console.log(outer());  // 3
outer = null;  // outer 식별자의 inner 함수 참조를 끊음

 

 

클로저 활용 사례

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

정보 은닉은 어떤 모듈의 내부 로직에 대해 외부로의 노출을 최소화해서 모듈간의 결합도를 낮추고 유연성을 높이고자 하는 현대 프로그래밍 언어의 중요한 개념중 하나이다

흔히 접근 권한에는 public, private, protected의 세 종류가 있다

public은 외부에서 접근 가능한 것이도, private은 내부에서만 사용하며 외부에 노출되지 않는 것을 의미한다

 

자바스크립트는 기본적으로 변수 자체에 이러한 접근 권한을 직접 부여하도록 설계돼 있지 않지만 클로저를 이용하면 함수 차원에서 public한 값과 private한 값을 구분하는 것이 가능하다

 

다시 위의코드인

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라는 변수를 통해 outer 함수를 실행할 수는 있지만, outer 함수 내부에는 어떠한 개입도 할 수 없다

외부에서는 오직 outer 함수가 return한 정보만 접근할 수 있다, return값이 외부에 정보를 제공하는 유일한 수단이다

그러니까 외부에 제공하고자 하는 정보들을 모아서 return 하고, 내부에서만 사용할 정보들은 return 하지 않는 것으로 접근 권한 제어가 가능하다

return한 변수들은 public이 되고, 그렇지 않은 변수들은 private가 되는 것이다

 

클로저를 활용해 접근 권한을 제어하는 방법은 다음과 같다

  1. 함수에서 지역변수 및 내부함수 등을 생성한다
  2. 외부에 접근권한을 주고자 하는 대상들로 구성된 참조형 데이터(대상이 여럿일 때는 객체 또는 배열, 하나일 때는 함수)를 return 한다
    → return한 변수들은 공개 멤버가 되고, 그렇지 않은 변수들은 비공개 멤버가 된다

 

부분 적용 함수

부분 적용 함수란 n개의 인자를 받는 함수에 미리 m개의 인자만 넘겨 이겅시켰다가, 나중에 (n-m)개의 인자를 넘기면 비로소 원래 함수의 실행 결과를 얻을 수 있게끔 하는 함수이다

 

bind를 사용한 부분 적용 함수 (클로저 x)

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);
console.log(addPartial(6, 7, 8, 9, 10));  // 55

위의 add 함수는 this를 사용하지 않으므로 bind 메서드만으로 문제없이 구현이 되었다

그러나 this의 값을 변경할 수밖에 없기 때문에 메서드에서는 사용할 수 없을 것 같다

 

this에 관여하지 않는 별도의 부분 적용 함수가 있다면 범용성 측면에서 더욱 좋을것 같다

클로저를 활용해보자

 

부분 적용 함수 구현 (클로저 o)

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);
        var restArgs = Array.prototype.slice.call(arguments);
        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('입니다!');  //왈왈, 강아지입니다!

partial의 첫 번째 인자에는 원본 함수를, 두번 째 인자 이후부터는 미리 적용할 인자들을 전달하고, 반환할 함수(부분 적용 함수)에서는 다시 나머지 인자들을 받아 이들을 한데 모아(concat) 원본 함수를 호출(apply)한다, 또한 실행 시점의 this를 그대로 반영함으로써 this에는 아무런 영향을 주지 않게 됐다

 

 

커링함수

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

앞서 살펴본 부분 적용 함수와 기본적인 맥락은 일치하지만 몇 가지 다른 점이 있다

 

커링함수와 부분적용함수의 차이점

커링함수 부분적용함수
 한 번에 하나의 인자만 전달하는 것을 원칙으로 한다, 또한 중간 과정상의 함수를 실행한 결과는 그다음 인자를 받기 위해 대기만 할 뿐으로, 마지막 인자가 전달되기 전까지는 원본 함수가 실행 되지 않는다  여러 개의 인자를 전달할 수 있고, 실행 결과를 재실행할 때 원본 함수가 무조건 실행된다

 

커링함수 구현

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

부분 적용 함수와 달리 커링 함수는 필요한 상황에 직접 만들어 쓰기 용이하다, 필요한 인자 개수만큼 함수를 만들어 계속 리턴해주다가 마지막에만 조합해서 리턴해주면 되기 때문이다

다만 인자가 많아질 수록 가독성이 떨어진다는 단점이 있다

 

 

커링함수 인자 5개 받기

var curry5 = function (func) {
    return function (a) {
        return function (b) {
            return function (c) {
                return function (d) {
                    return function (e) {
                        return func(a, b, c, d, e);
                    };
                };
            };
        };
    };
};

var getMax = curry5(Math.max);
console.log(getMax(1)(2)(3)(4)(5));  // 5

5개만 받았는데도 13줄을 소모했다

다행히 화살표 함수를 사용하면 한줄에 표현이 가능하다

 

커링함수 인자 5개 받기 (화살표함수)

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

화살표 함수로 구현하면 커링 함수를 이해하기에 훨씬 수월하다

화살표 순서에 따라 함수에 값을 차례로 넘겨주면 마지막에 func가 호출될 거라는 흐름이 한눈에 파악된다

 

위의 커링 함수는 각 단계에서 받은 인자들을 모두 마지막 단계에서 참조할 것이므로 GC(가비지 컬렉팅)되지 않고 메모리에 차곡차곡 쌓였다가, 마지막 호출로 실행 컨텍스트가 종료된 후에야 비로소 한꺼번에 GC의 수거대상이 된다

 

커링함수가 유용한 경우는 지연실행(lazy execution)을 할 때이다

당장 필요한 정보만 받아서 전달하고 또 필요한 정보가 들어오면 전달하는 식으로 하면 결국 마지막 인자가 넘어갈 때까지 함수 실행을 미루는 셈이 되는데 이것이 지연실행이다  

원하는 시점까지 지연시켰다가 실행하는 것이 요긴한 상황에 커링을 쓰기에 적합할 것이다

프로젝트 내에서 자주 쓰이는 함수의 매개변수가 항상 비슷하고 일부만 바뀌는 경우에도 커링을 사용하기에 유용하다

'코어 자바스크립트' 카테고리의 다른 글

7장 클래스  (0) 2020.12.28
6장 프로토타입  (2) 2020.12.24
4장 콜백함수  (0) 2020.12.22
3장 this  (2) 2020.12.21
1장 메모리와 데이터 타입  (0) 2020.12.21
2장 실행 컨텍스트  (0) 2020.12.21