본문으로 바로가기

클로저(closure)

category JavaScript/Core & 개념ㆍ용어 2016. 9. 29. 09:23


Closure

어떤 내부함수를 감싸는 외부함수가 실행되고 나서 종료되었다 하더라도 내부함수에서 외부함수의 변수(단, 변수의 최종값)에 접근할 수 있는 방법입니다.



즉,  다른 함수의 스코프에 있는 변수의 최종값에 접근 가능한 함수를 말합니다.


지역변수는 함수가 실행될때 생성되고 함수가 종료될때 사라지기 때문에 자바스크립트는 지역변수를 지우면 안된다고 인식하고 지역변수를 남겨두는 특성,현상을 클로저라고 합니다.



클로저는 이미 많은 곳에서 다양하게 다음과 같이 정의되고 있습니다.

1. 지역변수를 남겨두는 현상.

2. 외부,내부함수가 있을때 외부함수로 인해 생성된 공간을 클로저.

3. 리턴되는 함수자체를 클로저. (함수를 리턴하는 함수를 사용하는 가장 큰 이유 중 하나는 클로저 때문이다)

4. 살아남은 지역변수를 클로저라고 부르기도 함.

5. 내부함수가 외부함수의 변수에 접근 가능한 함수.


이렇게 정의는 다양하지만 이 의미들은 거의 비슷하게 설명되고 있습니다.



다음의 코드를 살펴보겠습니다.

function outerFn() {

// 클로저로 참조되는 외부변수를 참조
var a = 10;

//outerFn에서 선언된 a 변수를 참조하고 있는 innerFn이 클로저가 된다.
var innerFn = function() { console.log(a);}

return innerFn;
}

var inner = outerFn();
// inner 변수에는 함수가 반환된다.

inner(); // 10


이미 생명주기가 끝난 외부함수의 변수를 참조하는 함수를 클로저라고 합니다.

그리고 클로저로 참조되고 있는 외부함수

즉, 위 코드에서는 outerFn의 a와 같은 변수를 자유변수(free variable)라고 합니다.

closure라는 이름은 함수가 자유변수에 닫혀있다(closed,bound)는 의미로 해석됩니다.

달리 말해, 자유변수에 바인딩되어 있는 함수라고 할 수 있습니다.


[ Tip  ]

클로저는 자바스크립트에만 있는 개념이 아니고, 여러 언어에서 차용되고 있는 특성입니다.

특히, 함수를 일급객체로 취급하는 언어( 일반적으로 함수형 언어 )에서 주요하게 사용되는 특성입니다.




클로저를 사용한 경우


function count() {

  // 자유변수
  var cnt = 0;

  // 클로저 함수
  function addCount() {
    cnt++;
    return cnt;
  }
  return addCount;
}

var increase = count();

document.write("1. count = " + increase(), "<br>");
document.write("2. count = " + increase(), "<br>");
document.write("3. count = " + increase(), "<br>");

/**
 *------------------------
 * 실행결과 :
 * 1.	count = 1
 * 2.	count = 2
 * 3.	count = 3
 * -----------------------
 */

See the Pen 클로저 예제1 by jaeheekim (@jaehee) on CodePen.


위의 코드에서 count() 함수가 호출되면 지역변수인 cnt가 0 으로 초기화됨과 동시에 만들어집니다. 그리고 내부의 addCount() 라는 함수도 만들어지고, 마지막으로 addCount() 함수를 리턴하고 count() 함수는 종료되게 됩니다.


그렇다면 count() 함수가 종료되면 일반 함수처럼 addCount() 함수와 지역변수인 cnt는 사라지게 될까요?


그렇지 않습니다. count() 함수가 종료되더라도 사라지지 않고 계속해서 값을 유지하고 있게 됩니다. 


이유는 바로 count() 함수 내부에서 cnt변수를 사용하고 있는 상태에서 외부로 리턴되어 클로저 현상이 발생하기 때문입니다.


이런 이유로 increase() 가 실행되면 addCount() 함수가 실행되어 증가 연산자에 의해서 cnt 값이 0 에서 1 로 증가하기 때문에 1이 출력되고 다음의 increase() 가 계속 실행되면서 cnt 의 값이 0으로 초기화되지 않고 메모리에서 좀전의 첫번째 increase() 함수로 인해 증가된 증가값을  유지하고 있기 때문에 유지된 값이 증가하면서 2, 3 이 각각 출력되게 됩니다.


이처럼 변수가 메모리에서 제거되지 않고 계속해서 값을 유지하는 상태를 클로저라고 부르며 내부에 있는 함수를 클로저 함수라고 합니다.



$("#increase").click(function() {
  start();
  document.write("1씩 증가합니다.", '<br>');
});

function start() {
  // 자유변수
  var count = 0;
  // setInterval 의 매개변수로 넘겨준 익명함수가 클로저 함수
  setInterval(function() {
    count++;
    document.write(count, '<br>');
  }, 1200);
}

See the Pen 클로저 사용예제 2 by jaeheekim (@jaehee) on CodePen.



클로저를 사용한 코드를 보면 함수 내부에서 리턴값으로 함수를 리턴하는 코드들을 자주 접하게 됩니다. 

이렇다보니 함수 내부에서 함수를 리턴하는 구조(패턴)을 가진 경우만이 오직 클로저라고 오해하는 경우가 생길 수 있습니다. 


하지만 바로 위의 코드와 같은 경우에도 클로저 현상이 적용되어 있는 경우입니다.

버튼을 클릭하면 start() 가 실행되면서 지역변수인 count 변수가 만들어지고 setInterval() 이 실행된 후 함수가 종료되면 지역변수인 count 변수도 사라져야 하는 것이 맞지만, setInterval 의 익명함수에서 count 변수를 계속 사용하고 있기 때문에 값이 계속해서 증가하는 것을 볼 수 있습니다.


이때 이 익명함수를 클로저 함수라고 합니다.


이처럼 클로저는 변수가 사라지지 않고 계속해서 값을 유지하는 상태를 클로저라고 부릅니다.


// 외부함수
function increment(num) {
    // num인 함수의 인자(매개변수)는 함수 내에서 선언된 변수와 같다
    // 즉, var sum = 0;과 다를게 없다.

    // 외부함수 내에서 사용할 수 있는 전역변수와 같은 역할
    var sum = 0;
    return function() {
        sum += num;
        return sum;
    };
}

var result = increment(2);
document.write(result(), '<br>'); // 2
document.write(result(), '<br>'); // 4
document.write(result(), '<br>'); // 6

document.write('=========================<br>');

var result2 = increment(3);
document.write(result2(), '<br>'); // 3
document.write(result2(), '<br>'); // 6
document.write(result2(), '<br>');; // 9

See the Pen 클로저 사용예제 3 by jaeheekim (@jaehee) on CodePen.


위 코드에서 함수를 계속 호출하면 sum에 대한 값이 초기화되지 않고 계속 외부함수에서 sum에 대한  참조값을 지우지 않고 가지고 있는 것을 확인할 수 있습니다.




클로저를 사용하는 이유

지금까지 클로저의 개념과 간단하게 클로저를 사용한 예제를 살펴보았습니다.


그렇다면 클로저를 사용하면 좋은 점이 무엇일까요?


바로 연관(관계)있는 변수와 기능(중첩함수)을 하나의 함수로 묶어서 독립적으로 실행시킬 수 있다는 점입니다.


다시 말해서, 함수 내부에 데이터가 만들어지기 때문에 함수 외부에서 수정할 수 없도록 보호된 데이터를 만들 수 있습니다.

(이러한 보호된 데이터를 만드는 것을 객체지향 프로그래밍에서는 private 데이터라고 부릅니다.)


See the Pen 클로저를 이용한 탭메뉴 by jaeheekim (@jaehee) on CodePen.


위의 코드를 살펴보면 tabMenu() 의 함수 호출이 끝났음에도 불구하고 선택된 탭메뉴 항목을 담고 있는 변수인 $selectMenuItem 이 사라지지 않고 계속해서 상태 값을 유지하고 있는 것을 확인 할 수 있습니다.


다시말해, tabMenu() 함수 호출이 완료되면 지역변수인 $selectMenuItem 이 사라져야 하는데 tabMenu() 함수 내부의 중첩함수(클로저함수)에서 $selectMenuItem 변수를 사용하고 있기 때문에 클로저 현상이 발생하여 지역변수가 사라지지 않아 선택된 아이템에 대한 값을 유지하게 되는 것입니다.


익명함수인 click 이벤트 리스너는 콜백함수인 동시에 클로저함수로써 $selectMenuItem 변수를 사용하고 있는 전형적인 클로저입니다.


이와 같이 클로저를 이용하면 독립적으로 실행되는 탭메뉴를 구현할 수도 있으며 함수 하나에 연관있는 변수와 함수를 묶어서 유용하게 사용할 수 있습니다.





객체의 메소드에서 사용된 클로저


function drama(title) {
  return {
    get_title: function() {
      return title;
    },
    set_title: function(_title) {
      title = _title
    }
  }
}
var sbs = drama('육룡이 나뻐샤');
var mbc = drama('그녀는 엿됐다!');

document.write(sbs.get_title(), '<br>');
document.write(mbc.get_title(), '<br>');

sbs.set_title('애인 없어요!');

document.write(sbs.get_title(), '<br>');
document.write(mbc.get_title());

See the Pen 객체를 이용한 클로저 by jaeheekim (@jaehee) on CodePen.


위의 코드를 살펴보면 tabMenu() 의 함수 호출이 끝났음에도 불구하고 선택된 탭메뉴 항목을 담고 있는 변수인 $selectMenuItem 이 사라지지 않고 계속해서 상태 값을 유지하고 있는 것을 확인 할 수 있습니다.


다시말해, tabMenu() 함수 호출이 완료되면 지역변수인 $selectMenuItem 이 사라져야 하는데 tabMenu() 함수 내부의 중첩함수(클로저함수)에서 $selectMenuItem 변수를 사용하고 있기 때문에 클로저 현상이 발생하여 지역변수가 사라지지 않아 선택된 아이템에 대한 값을 유지하게 되는 것입니다.


익명함수인 click 이벤트 리스너는 콜백함수인 동시에 클로저함수로써 $selectMenuItem 변수를 사용하고 있는 전형적인 클로저입니다.


이와 같이 클로저를 이용하면 독립적으로 실행되는 탭메뉴를 구현할 수도 있으며 함수 하나에 연관있는 변수와 함수를 묶어서 유용하게 사용할 수 있습니다.



! 참고

Private 속성은 객체의 외부에서는 접근 할 수 없는 외부에 감춰진 속성이나 메소드를 의미한다. 이를 통해서 객체의 내부에서만 사용해야 하는 값이 노출됨으로서 생길 수 있는 오류를 줄일 수 있다. 자바와 같은 언어에서는 이러한 특성을 언어 문법 차원에서 지원하고 있다.




클로저 사용시 주의해야 할 사항


var arr = []
for(var i = 0; i < 5; i++){
    arr[i] = function(){
        return i;
    }
}
for(var index in arr) {
    document.write(arr[index]() , '<br>');
}

See the Pen 클로저 반복문 실수되는 예제 by jaeheekim (@jaehee) on CodePen.


위의 코드 예제에서 아마도 결과창에 0,1,2,3,4 가 출력될 것이라고 예상할 수 있습니다.


즉, 함수가 함수의 외부 컨텍스트에 접근할 수 있을 것이라고 기대하겠지만 결과는 5만 5번 출력되는 것을 확인할 수 있습니다.


그 이유는  i 변수값이 function 함수 외부의 변수값이 아니기 때문입니다.

그렇기 때문에 현재 익명함수로 정의된 함수를 내부함수로 만들 수 있도록 외부함수를 정의하고 그 외부함수의 지역변수 값을 내부함수가 참조하도록 변경하면 됩니다.



그럼 변경한 다음의 코드를 살펴보겠습니다.


var arr = []
for(var i = 0; i < 5; i++){
    arr[i] = function(id) {
        return function(){
            return id;
        }
    }(i);
}
for(var index in arr) {
    document.write(arr[index](), '<br>');
}

See the Pen 클로저 반복문 by jaeheekim (@jaehee) on CodePen.




Jaehee's WebClub