하루에 한 문제

클로저 본문

Dev/JavaScript

클로저

dkwjdi 2021. 4. 15. 16:10

클로저란 ?

클로저는 독립적인 (자유) 변수를 가리키는 함수이다. 또는, 클로저 안에 정의된 함수는 만들어진 환경을 ‘기억한다’.

 

흔히 함수 내에서 함수를 정의하고 사용하면 클로저라고 한다. 하지만 대개는 정의한 함수를 리턴하고 사용은 바깥에서 하게된다. 

function outerFunc() {
    var x = 10;
    var innerFunc = function () {
        console.log(x);
    };

    return innerFunc
}


var inner = outerFunc();
inner(); //10
  • 우선 자바스크립트의 함수는 일급 객체로 취급하기 때문에 함수의 인자로 넘기거나, return값으로 받을 수 있다.
  • inner 변수에 outerFunc()의 결과인 innerFunc가 담긴다.
  • 이때 inner()함수를 실행하게 되면 outerFunc()의 지역변수 x에 접근해서 값을 가지고 오는 것을 확인 할 수 있다.
  • 즉, 이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 것을 클로저라고 한다!

 

그렇다면 아래 코드의 결과값은 어떻게 될까?

function outerFunc(arg1, arg2) {
    var local = 8;
    function innerFunc(innerArg) {
        console.log((arg1 + arg2) / (innerArg + local));
    }
    return innerFunc;
}

var exam1 = outerFunc(2, 4);
exam1(2);

앞서 배운 실행컨텍스트를 통해서 설명을 해보면

 

  • exam1(2)을 호출하면 
  • arg1, arg2, local 값은 -> outerFunc 변수 객체에서 찾고
  • innerArg는 innerFunc 변수 객체에서 찾는다!

 

 

 

클로저를 통한 은닉화

  • 일반적으로 JavaScript에서 객체지향 프로그밍을 말한다면 Prototype을 통해 객체를 다루는 것을 말한다.
  • Prototype을 통한 객체를 만들 때의 주요한 문제 중 하나는 Private variables에 대한 접근 권한 문제다.
function Hello(name) {
    this._name = name;
}

Hello.prototype.say = function () {
    console.log('Hello,' + this._name);
}

var hello1 = new Hello('준영1');
var hello2 = new Hello('준영2')
var hello3 = new Hello('준영3')

hello1.say(); // 'Hello, 준영1'
hello2.say(); // 'Hello, 준영2'
hello3.say(); // 'Hello, 준영3'
hello1._name = 'anonymous';
hello1.say(); // 'Hello, anonymous'
  • 위의 Hello()로 생성된 객첻르은 모두 _name이라는 프로퍼티를 가진다.
  • 변수명앞에 _ 를 포함했다는 것은 변수를 Private variable로 쓰고 싶다는 의도이다.
  • 하지만 여전히 .name을 통해서 접근이 가능하다.

 

이런 경우 클로저를 사용해 외부에서 변수에 직접 접근하는 것을 제한할 수 있다.

function Hello(name){
	var_name = name;
    return function(){
    	console.log('Hello' + _name);
    }
}

var hello1 = hello('준영1');
var hello2 = hello('준영2');
var hello3 = hello('준영3');

hello1(); // 'Hello, 준영1'
hello2(); // 'Hello, 준영2'
hello3(); // 'Hello, 준영3'

 

 

setTimeout()에 지정되는 함수의 사용자 정의

  • setTimeout 함수의 첫 번째 인자로 넘겨지는 함수 실행의 스케쥴링을 할 수있다.
  • setTimeout으로 자신의 코드를 호출하고 싶다면 첫 번째 인자로 해당 함수 객체의 참조를 넘겨주면 되지만, 이것으로는 실제 실행될 때 함수에 인자를 줄 수 없다.
  • 그렇다면 내가 정의한 함수에 인자를 넣어줄 수 있게 하려면 어떻게 해야 할까
  • 답은 클로저다!
function callLater(obj, a, b) {
    return (function () {
        obj["sum"] = a + b;
        console.log(obj["sum"]);
    });
}

var sumObj = {
    sum : 0
}

var func = callLater(sumObj, 1, 2);
func();
console.dir(sumObj);
setTimeout(func, 500);
// console.dir(func);
  • callLater를 setTimeout함수로 호출하려면, 변수 func에 함수를 반환받아 setTimeout()함수의 첫 번째 인자로 넣어주면 된다.
  • 당연히 반환받는 함수는 클로저이고 사용자가 원하는 인자로 접근이 가능하다.

 

 

 

 

하나의 클로저가 여러 함수 객체의 스코프 체인에 들어가 있는 경우도 있다.

아래의 코드는 클로저를 통해 getter, setter를 만드는 코드이다.

function factory_book(title) {
    
    return {
        get_title : function() {
            return title;
        },
        set_title: function(_title) {
            title = _title;
        }
    }
}

var c = factory_book('c');
var java = factory_book('java');

console.log(c.get_title()); //c
console.log(java.get_title()); //java
java.set_title('javascript');
console.log(java.get_title('javascript')); //javascript
  • c와 java라는 변수에 factory_book의 반환값인, getter, setter가 들어가 있다.
  • 이런식으로 반환값으로 여러 함수객체의 스코프 체인이 들어갈 수 있다.

 

 

 

루프 안에서 클로저를 활용할 때는 주의하자

아래의 코드의 결과는 어떻게 될까?

function countSeconds(howMany) {
    for (var i = 1; i <= howMany; i++){
        setTimeout(function () {
            console.log(i);
        }, i * 1000);
    }
};

countSeconds(3);

나는 처음에 이 코드를 봤을 때 "당연히 1초 후 1, 2초 후 2, 3초 후 3 이 출력되는거 아닌가?" 라고 생각했다. 뭐 다들 예상하겠지만 당연히 틀렸다.

 

  • 위 코드의 결과는 4가 연속 3번 1초 간격으로 출력된다. 이유가 뭘까?
  • 사실 책에 그에 대한 이유가 적혀있긴 한데 이해가 안되서 다른 글들을 찾아봤다. 결국 brunch.co.kr/@cadenzah/2 이 블로그를 통해서 이해를 했다.
  • 즉, 클로저에서, 상위 함수의 변수를 참조하는 것과 변수의 값을 가져오는 것은 다르다.
  • setTimeout()함수 안에서 i는 자유변수 i를 '참조한다' 하지만 함수가 실행되는 시점은 countSecondes()함수의 실행이 종료된 이후이고, i값은 이미 4가 된 상태이다.

 

이를 해결하기 위해 i값의 복사본을 함수에 넘겨준다. 

function countSeconds(howMany) {
    for (var i = 1; i <= howMany; i++){
        (function (currentI) {
            setTimeout(function () {
                console.log(currentI);
            }, currentI * 1000);
         })(i);
    }
};

countSeconds(5);

즉시 실행 함수를 실행시켜 루프 i값을 currnetI에 복사해서 setTimeout()에 들어갈 함수에서 사용하면 위에서 원했던 결과를 얻을 수 있다.

 

 

클로저의 남용은 성능문제를 유발시킬 수 있는 여지가 있다.

  • 대부분의 클로저에서는 스코프 체인에서 뒤쪽에 있는 객체에 자주접근하므로 성능을 저하시키는 이유로 지목되기도 한다.
  • 게다가 클로저를 사용한 코드는 그렇지 않은 코드보다 메모리 부담이 간다. (생명주기가 끝난 함수를 참조 -> GC가 일어나지 않음)

 

 

참고

book.naver.com/bookdb/book_detail.nhn?bid=7400243

brunch.co.kr/@cadenzah/2

'Dev > JavaScript' 카테고리의 다른 글

async VS defer  (0) 2021.04.17
NPM 이란?  (1) 2021.04.16
스코프 체인  (2) 2021.04.14
실행 컨텍스트  (0) 2021.04.13
프로토타입 체이닝  (0) 2021.04.13
Comments