본문 바로가기

개발/Javascript

쉽게 이해하는 클로져(closure)

출처:

javacriptissexy.com 번역


inner fuction = 내장함수 = 중첩함수


클로저란 무엇인가?

클로저는 바깥 합수의 변수에 접근할 수 있는 중첩합수이다. 클로저는 세개의 스콥체인(유효범위체인)을 갖고 있다. 1.자기 자신, 2.바깥 함수의 변수에 접근하는 것, 3.전역 변수에 접근하는 것 이렇게 세 개이다.


중첩함수는 바깥함수의 변수 뿐만 아니라 매개변수(parameter)에도 접근할 수 있다. 중첩함수는 바깥함수의 매개변수(parameter)는 사용할 수 있지만, 바깥함수의 arguments 객체를 호출할 수는 없다.


다른 함수 안에 함수를 추가해서 클로저를 만들수 있다.

자바스크립트의 클로저 기본 예제

function showName (firstName, lastName) {


var nameIntro = "Your name is ";

    

​    //이 중첩함수는 매개변수(parameter)를 포함해서 바깥 함수의 변수에 접근가능하다

function makeFullName () {
        

return nameIntro + firstName + " " + lastName;
    

}

​return makeFullName ();


}


showName ("Michael", "Jackson"); // Your name is Michael Jackson


클로저는 Node.js에서 많이 쓰인다. Node.js의 비동기와 non-blocking 구조에서 자주 쓰인다. 또한 jQuery나 자바스크립트의 코드 부분부분에서도 자주 사용된다.


클로저 jQuery 예제

$(function() {

​var selections = []; 


$(".niners").click(function() { 

//이 클로저는 selections에 접근할 수 있다.

selections.push (this.prop("name")); 

//바깥 함수의 범위에 있는 변수인 selections을 수정했다.

});

});


클로저의 규칙과 부작용

1. 클로저는 바깥함수가 return된 후에도 바깥함수의 변수에 접근할 수 있다.

클로저의 가장 중요하고 다루기 어려운 특징중 하나가 중첩함수는 바깥함수가 return된 후에도 여전히 바깥 함수의 변수에 접근할 수 있다는 점이다. 자바스크립트의 함수가 실행될 때, 그 함수가 생성될 때 형성된 그 스콥체인을 사용한다. 이 의미는 바깥 함수가 리턴된 후에도 중첩함수는 여전히 바깥 함수의 변수에 접근할 수 있다는 뜻이다. 그래서 프로그램상에서 계속 중첩함수를 호출할 수 있다. 다음 예제를 보자

function celebrityName (firstName) {

    var nameIntro = "This celebrity is ";

    

   //중첩함수는 바깥함수의 매개변수와 변수에 접근할 수 있다.

   function lastName (theLastName) {

        return nameIntro + firstName + " " + theLastName;

    }

    return lastName;

}

​var mjName = celebrityName ("Michael"); //이 시점에서 바깥함수인 celerityName는 리턴됐다.

​//클로저인 lastName은 바깥함수가 리턴된 후에 여기에서 불린다.

//클로저는 여전히 바깥함수의 변수와 매개변수에 접근이 가능하다.

mjName ("Jackson"); // This celebrity is Michael Jackson



2. 클로저는 바깥함수 변수의 reference(참조)를 저장한다; 실제 값을 저장하는게 아니다.

클로저가 불리기 전에 바깥함수의 변수 값이 변한다는 점은 클로저를 더욱 흥미롭게 만들어준다. 바로 이것이 아래와 같은 예제처럼 창의적인 방법으로 사용할만한 강력한 기능이다. 

function celebrityID () {

    var celebrityID = 999;


    //중첩함수를 갖는 객채를 리턴

    //모든 중첩함수는 바깥 함수의 변수에 접근할 수 있다.

    return {

        getID: function ()  {

            //이 중첩함수는 수정된 celebrityID를 리턴할 것이다.

          return celebrityID;

        },

        setID: function (theNewID)  {

            //이 중첩함수는 바깥 함수의 변수 값을 바꾼다.

            celebrityID = theNewID;

        }

    }

}

​var mjID = celebrityID (); //이 시점에서 celebrityID가 리턴되었다.

mjID.getID(); // 999​

mjID.setID(567); //바깥함수의 변수 값을 바꾸었다.

mjID.getID(); // 567: 수정된 celebrityID 를 리턴한다.


3. 엉망이 되어버린 클로저

클로저는 바깥 함수의 수정된 변수에 접근할 수 있기 때문에, 바깥 함수의 변수가 루프구문에서 변하게 되면서 버그를 유발하기도 한다.

//코드에 대한 설명은 코드 아래에서..

​function celebrityIDCreator (theCelebrities) {

    var i;

    var uniqueID = 100;

    for (i = 0; i < theCelebrities.length; i++) {

      theCelebrities[i]["id"] = function ()  {

        return uniqueID + i;

      }

    }

    

    return theCelebrities;

}

​var actionCelebs = [{name:"Stallone", id:0}, {name:"Cruise", id:0}, {name:"Willis", id:0}];

​var createIdForActionCelebs = celebrityIDCreator(actionCelebs);

​var stalloneID = createIdForActionCelebs[0];

console.log(stalloneID.id()); // 103

위의 예제에서, 익명함수가 불릴 때 i값은 3이다. uniqueID+3으로 모든 celebritiesID에 103이 된다. 그래서 리턴된 배열의 id는 100,101,102가 아니라 모두 103이 된다.


이러한 현상이 일어나는 이유는 전 예제에서 봤듯이, 클로저(이 예제에서는 익명함수)가 바깥 함수의 변수에 값이 아닌 참조로 접근하기 때문이다. 그래서 루프가 다 돌고 난 후의 마지막 값인 3에 클로저가 i변수에 접근하게 된다.

celebrityIDCreator함수가 리턴된 값인 createIdForActionCelebs 배열을 보면 id에 익명함수가 저장되어 있음

stalloneId.id() 에서 비로서 함수가 불리게 되고, 이 때 i값은 3이 되어있다.


이러한 부작용(버그)를 수정하기 위해 다음과 같이 즉시실행함수를 사용할 수 있다.

function celebrityIDCreator (theCelebrities) {

    var i;

    var uniqueID = 100;


    for (i = 0; i < theCelebrities.length; i++) {


       //매개변수 j는 즉시실행함수 호출에서 넘겨진 i이다.

        theCelebrities[i]["id"] = function (j)  { 

            return function () {

               //각 회전마다 현재 i값을 즉시실행함수에 전달하게 되고 배열에 정확한 값을 저장하게 된다.(위의 예제처럼 id에 함수를 저장하는게 아니라)

                return uniqueID + j

            } ()  //함수 끝에 ()를 붙이면, 함수가 바로 실행되고, 함수 자체가 아니라 

                uniqueID + j를 리턴할 수 있다.

        } (i); //파라미터로 i를 넘겨서 함수를 바로 호출

    }

    return theCelebrities;

}

​var actionCelebs = [{name:"Stallone", id:0}, {name:"Cruise", id:0}, {name:"Willis", id:0}];

​var createIdForActionCelebs = celebrityIDCreator (actionCelebs);

​var stalloneID = createIdForActionCelebs [0];


console.log(stalloneID.id); // 100​

​var cruiseID = createIdForActionCelebs [1];
console.log(cruiseID.id); // 101